-- Effects
-- create by zouyb
-- 特效表现相关

require "game/ui/module/EffectAnimation"
require "game/ui/form/dungeon/UIDungeonMgr"

-- 飞行速度
local speed = 800;

-- 最小距离
local minDis = 400;

local effectStat = {};

-- 保存触发技能特效的相关信息
-- eg:{skillId, dungeonId, curFloor, applyProb, isRetain, isEffectPlay, isInTemp，isPlaying }
-- skillId:技能Id,  dungeonId:地牢Id,curFloor:当前楼层 applyProb:触发概率，isRetain:是否保留, isEffectPlay:是否当前层播放过虚影效果, isInTemp:是否在夹层,isPlaying:是否正在播放
local effectTempInfo = {};

-- 当前正在播放触发专属技能
local isPlayingSpecial = false;

-- 需要缓存的特效ID
local cacheEffectId = {999, 998, 1172, 1105, 1182, 1183, 1100, 1230, 1181, 1157, 1106, 1120, 1119, 1118, 1117, 1115, 1116, 1123, 1104, 1101, 1135};

-- 增加特效统计
function incEffectStat(effectId)
    if type(effectId) ~= "number" then
        print("effect 配置错误，非数字！"..debug.traceback());
        return;
    end

    if DungeonM and not DungeonM.isInDungeon() then
        return;
    end

    if effectStat[effectId] == nil then
        effectStat[effectId] = 0;
    end

    effectStat[effectId] = effectStat[effectId] + 1;
end

-- 打印特效文件使用情况
function printEffectStat()
    local function sortFunc(f1, f2)
        return f1.count > f2.count;
    end

    local t = {};
    for effectId, count in pairs(effectStat) do
        local temp = {};
        temp.id = effectId;
        temp.count = count;
        table.insert(t, temp);
    end

    table.sort(t, sortFunc);

    for _, temp in pairs(t) do
        print(string.format("[%s] count:%s, path:%s", tostring(temp.id), tostring(temp.count), tostring(EffectM.query(temp.id, "effect_src"))));
    end
end

--播放特效
function playEffect(parent, effectId, adjustX, adjustY, frameEventFunc, scaleA)
    Profiler.funcBegin("playEffect");

    if parent == nil or effectId == nil then
        return;
    end

    local size = AlignM.frameSize;
    local posY = 0;
    local posX = 0;

    local effectPath;
    local effectSrc;
    local scale;
    local duration;
    local rotate;
    local offset;
    local speed = 1;
    local loop = 0;
    local fx;
    local fxLoop = 0;
    local fxStartFrame = -1;
    local fxEndFrame = -1;

    if effectId ~= nil and effectId ~= "" then
        effectPath = EffectM.query(effectId, "path");
        effectSrc  = EffectM.query(effectId, "effect_src");
        scale      = EffectM.query(effectId, "scale");
        duration   = EffectM.query(effectId, "play_time");
        rotate     = EffectM.query(effectId, "rotate");
        offset     = EffectM.query(effectId, "off_set");
        speed      = EffectM.query(effectId, "speed");
        loop       = EffectM.query(effectId, "loop");
        fx         = EffectM.query(effectId, "fx");
        fxStartFrame = tonumber(EffectM.query(effectId, "fx_start_frame"));
        fxEndFrame = tonumber(EffectM.query(effectId, "fx_end_frame"));

        incEffectStat(effectId);

        if speed == nil then
            speed = 1;
        end

        if offset then
            posX = posX + tonumber(offset[1]);
            posY = posY + tonumber(offset[2]);
        end
    end

    if effectSrc == nil then
        return nil, nil;
    end

    local path = effectPath.."/"..effectSrc..".csb";
    local effect, animAction;
    effect, animAction = AnimationCacheM.createAnimationNodeAndAction(path);

    if scaleA ~= nil then
        scale = scale * scaleA;
    end
    effect:setScale(scale);
    effect:setRotation(rotate);

    animAction:setTimeSpeed(speed);
    effect:runAction(animAction);
    effect.action = animAction;
    effect:setPosition(posX + (adjustX or 0), posY + (adjustY or 0));

    -- 有绑定音效或者帧回调处理时才调用setFrameEventCallFunc
    if fxStartFrame == 1 and fxEndFrame == 0 then
        AudioM.playFx(fx, false);
    elseif (fxStartFrame ~= nil and fxStartFrame > 0) or frameEventFunc ~= nil then
        local function frameEventCallback(frame)
            if frame.getEvent and frame:getEvent() == "pilor" then
                local index = frame:getFrameIndex();
                -- 处理绑定在光效上面的音效
                if fxStartFrame == index and effect.fxHandle == nil and type(fx) == "string" and string.len(fx) > 0 then
                    effect.fxHandle = AudioM.playFx(fx, fxStartFrame == fxEndFrame );
                elseif fxStartFrame ~= fxEndFrame and fxEndFrame == index and effect.fxHandle then
                    AudioM.stopFx(effect.fxHandle);
                    effect.fxHandle = nil;
                end
            end
            if frameEventFunc then
                frameEventFunc(frame);
            end
        end

        animAction:setFrameEventCallFunc(frameEventCallback);
    end

    if duration > 0 then
        animAction:gotoFrameAndPlay(1, iif(loop==1, true, false));

        if loop ~= 1 then
            local callfunc = cc.CallFunc:create(function()
                -- 如果不停留在最后一帧，则播放完后，直接移除
                parent:removeChild(effect);

                AnimationCacheM.freeAnimationNodeAndAction(path, effect, animAction);
            end);
            effect:runAction(cc.Sequence:create(cc.DelayTime:create(duration/1000), callfunc));
        end
    else
        animAction:gotoFrameAndPlay(1, false);
    end

    effect:registerScriptHandler(function(ev)
        if ev == "exit" then
            -- 停止音效
            if effect.fxHandle and fxStartFrame == fxEndFrame then
                AudioM.stopFx(effect.fxHandle);
                effect.fxHandle = nil;
            end
        end
    end);

    parent:addChild(effect);

    Profiler.funcEnd("playEffect");

    return duration/1000, effect;
end

-- 通知特效完成
function finishEffect(desc)
    EventMgr.fire(event.UI_EFFECT_DONE, { ["desc"] = desc, });
end

-- 物品发光
function shiningEffect(parentNode, adjustX, adjustY)
    playEffect(parentNode, 1100, adjustX, adjustY);
end

-- 宝箱发光
function boxShiningEffect(parentNode, adjustX, adjustY)
    playEffect(parentNode, 1100, adjustX, adjustY);
    playEffect(parentNode, 1108, adjustX, adjustY);
end

-- 开门特效
function openDoorEffect(parentNode, adjustX, adjustY)
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    local worldPos = parentNode:convertToWorldSpaceAR(cc.p(0, 0));

    local targetPos = effectParent:convertToNodeSpace(worldPos);
    playEffect(effectParent, 1101, targetPos.x + adjustX, targetPos.y + adjustY);
    AudioM.playFx("open_door");
end

-- 传送门特效
function portalEffect(parentNode, adjustX, adjustY)
    playEffect(parentNode, 1102, adjustX, adjustY);
end

-- 宝箱开启特效
function openBoxEffect1(parentNode, boxType, adjustX, adjustY)
    local effectId = 1294;

    if boxType == "silver" then
        effectId = 1295;
    elseif boxType == "golden" then
        effectId = 1296;
    elseif boxType == "darkgoldenrod" then
        effectId = 1297;
    elseif boxType == "holy" then
        effectId = 1298;
    end

    local _, effect = playEffect(parentNode, effectId, adjustX, adjustY);
    local children = effect:getChildren();
    local len = table.getn(children)
    for i = 1, len do
        local child = children[i];
        if child:getName() == "layer 4" then
            local children2 = child:getChildren();
            local len2 = table.getn(children2)
            for j = 1, len2 do
                local child2 = children2[j];
                -- 对layer 4节点下的sprite做高光和叠加效果
                -- 由于美术保存图片后亮光效果消失，故而转到程序实现
                setHighLightMode(child2, true, 0.4);
                child2:setBlendFunc(gl.SRC_COLOR, gl.ONE);
            end
        end
    end
end

-- 钥匙出现
function keyAppearEffect(parentNode, adjustX, adjustY)
    playEffect(parentNode, 1104, adjustX, adjustY);
end

-- 文字变大后再变小
function commonTextEffect(textNode, color, anchorPoint, pos)
    -- 文本先放大再缩小

    if textNode.oldScaleX == nil then
        textNode.oldScaleX = textNode:getScaleX();
    end

    if textNode.oldScaleY == nil then
        textNode.oldScaleY = textNode:getScaleY();
    end

    -- 将锚点临时调整到0.5,0.5的位置
    local oldAnchorPoint = textNode:getAnchorPoint();
    local size = textNode:getContentSize();
    local oldPos = cc.p(textNode:getPosition());

    local relativePos = cc.p((0.5-oldAnchorPoint.x) * size.width * textNode.oldScaleX, (0.5-oldAnchorPoint.y) * size.height * textNode.oldScaleY);
    local newPos = cc.pAdd(oldPos, relativePos);
    textNode:setPosition(newPos);

    textNode:setAnchorPoint(0.5, 0.5);

    if textNode.oldColor == nil and textNode.getTextColor then
        textNode.oldColor = textNode:getTextColor();
    end

    if color ~= nil then
        textNode:setTextColor(color);
    end

    textNode.effectCount = textNode.effectCount or 0;
    textNode.effectCount = textNode.effectCount + 1;

    local scaleUp = cc.ScaleTo:create(0.1, 1.4 * textNode.oldScaleX, 1.4 * textNode.oldScaleY);
    local delay = cc.DelayTime:create(0.1);
    local scaleDown = cc.ScaleTo:create(0.3, 1 * textNode.oldScaleX, 1 * textNode.oldScaleY);
    local callFunc = cc.CallFunc:create(function()
        textNode.effectCount = textNode.effectCount - 1;
        if textNode.effectCount > 0 then
            return;
        end

        -- 还原锚点
        textNode:setAnchorPoint(oldAnchorPoint);
        textNode:setPosition(oldPos);
        textNode:setScaleX(textNode.oldScaleX);
        textNode:setScaleY(textNode.oldScaleY);
        if textNode.setTextColor then
            textNode:setTextColor(textNode.oldColor);
        end

        if anchorPoint ~= nil then
            textNode:setAnchorPoint(anchorPoint);
        end
        if pos ~= nil then
            textNode:setPosition(pos);
        end

    end);

    textNode:runAction(cc.Sequence:create(scaleUp, delay, scaleDown, callFunc));
end

-- 文字变大后再变小
function commonTextEffect2(textNode, color)
    -- 文本先放大再缩小

    -- 将锚点临时调整到0.5,0.5的位置
    local oldAnchorPoint = textNode:getAnchorPoint();
    local size = textNode:getContentSize();
    local oldPos = cc.p(textNode:getPosition());

    local relativePos = cc.p((0.5-oldAnchorPoint.x) * size.width * textNode:getScaleX(),
        (0.5-oldAnchorPoint.y) * size.height * textNode:getScaleY());
    textNode:setPosition(cc.pAdd(oldPos, relativePos));

    textNode:setAnchorPoint(0.5, 0.5);

    if textNode.oldScaleX == nil then
        textNode.oldScaleX = textNode:getScaleX();
    end

    if textNode.oldScaleY == nil then
        textNode.oldScaleY = textNode:getScaleY();
    end

    if textNode.oldColor == nil then
        textNode.oldColor = textNode:getTextColor();
    end

    if color ~= nil then
        textNode:setTextColor(color);
    end

    local scaleUp = cc.ScaleTo:create(0.1, 1.4 * textNode.oldScaleX, 1.4 * textNode.oldScaleY);
    local delay = cc.DelayTime:create(0.1);
    local scaleDown = cc.ScaleTo:create(0.3, 1 * textNode.oldScaleX, 1 * textNode.oldScaleY);
    local callFunc = cc.CallFunc:create(function()
        -- 还原锚点
        textNode:setAnchorPoint(oldAnchorPoint);
        textNode:setPosition(oldPos);
        textNode:setScaleX(textNode.oldScaleX);
        textNode:setScaleY(textNode.oldScaleY);
        textNode:setTextColor(textNode.oldColor);
    end);

    textNode:runAction(cc.Sequence:create(scaleUp, delay, scaleDown, callFunc));
end

-- 文字循环变大变小
function commonTextEffect3(textNode, upDuration, downDuration, upScale, downScale)
    local scaleUp = cc.ScaleTo:create(upDuration, upScale);
    local scaleDown = cc.ScaleTo:create(downDuration, downScale);
    textNode:runAction(cc.RepeatForever:create(cc.Sequence:create(scaleUp, scaleDown)));
end

-- 拾取探索点特效
function pickupSpEffect(sourceNode, iconNode, textNode, bigOne)
    local icon = bigOne or "search_point1";
    local iconPath = getLevelItemIconPath(icon);

    -- 图标飞入到菜单右上角探索点处
    local originToPosX = iconNode:getPositionX();
    local originToPosY = iconNode:getPositionY();

    -- 目标位置先转成世界坐标
    local worldPos = iconNode:convertToWorldSpaceAR(cc.p(0, 0));
    --print("originToPosX:"..originToPosY..",toPosY:"..originToPosY..", world posx:"..worldPos.x..",posy:"..worldPos.y);

    -- 再转到相对于sourceNode的父节点的坐标
    local parentNode = sourceNode:getParent();
    local toPos = parentNode:convertToNodeSpace(worldPos);
    --print("toPosX:"..toPos.x..",toPosY:"..toPos.y);

    local fromPos = cc.p(sourceNode:getPosition());
    --print("fromPosX:"..fromPos.x..",fromPosY:"..fromPos.y);

    local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / speed;

    local icon = ccui.ImageView:create();
    icon:loadTexture(iconPath);
    icon:setPosition(fromPos);
    icon:setLocalZOrder(10000);

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();

    local callfunc = cc.CallFunc:create(function()
        effectParent:removeChild(icon);

        -- 探索点图标闪一下
        playEffect(iconNode, 1105, 26, 18);
        commonTextEffect2(textNode, TextStyleM.TEXT_COLOR_BRIGHT_YELLOW);

        finishEffect("gain_sp");
    end);

    local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), 2.0);
    local rotate = cc.RotateBy:create(duration, 180);
    local scaleTo = cc.ScaleTo:create(duration, 0.5);
    icon:runAction(cc.Sequence:create( cc.Spawn:create(moveTo,rotate,scaleTo), callfunc));
    effectParent:addChild(icon);
end

-- 依次显示物品，飘字上升
function showItemName(targetNode, color, x, y, createTextFunc, anchorPoint)
    x = x or targetNode:getContentSize().width / 2;
    y = y or targetNode:getContentSize().height;

    local function defaultCreateText(color, str)
        local text = ccui.Text:create();
        text:setFontName(getLocFontName());
        text:setFontSize(36 * getLocFontSizeMulti());
        text:setString(str);
        text:setTextColor(color);
        local ap = anchorPoint or cc.p(0.5, 0.5);
        text:setAnchorPoint(ap);
        local outlineColor = TextStyleM.TEXT_COLOR_DARK_BROWN;
        text:enableOutline(cc.c4b(outlineColor[1], outlineColor[2], outlineColor[3], 255), 2);
        text:getVirtualRenderer():setAdditionalKerning(getLocKerning() -4);
        text:setScale(0.8);
        return text;
    end

    local function show()
        if #targetNode.gainItemList > 0 then
            targetNode.isShowing = true;

            local str = targetNode.gainItemList[1];

            local createFunc = defaultCreateText;
            if createTextFunc then
                createFunc = createTextFunc;
            end

            local text = createFunc(color, str);
            local curScale = text:getScale();
            text:setScale(0.8 * curScale);

            local scaleUp = cc.ScaleTo:create(0.2, 1.2 * curScale);
            local scaleDown = cc.ScaleTo:create(0.15, 1.0 * curScale);
            local move = cc.MoveBy:create(1.0, cc.p(0, 50));
            local delay = cc.DelayTime:create(0.5);
            local fadeOut = cc.FadeOut:create(0.5);
            local callFunc2 = cc.CallFunc:create(function()
                targetNode.isShowing = false;
                targetNode:removeChild(text);
            end);

            local action = cc.Sequence:create(scaleUp, scaleDown, cc.Spawn:create(move, cc.Sequence:create(delay, fadeOut)), callFunc2);
            text:runAction(action);
            text:setPosition(x, y);
            targetNode:addChild(text);
            table.remove(targetNode.gainItemList, 1);

            performWithDelay(targetNode, show, 0.5);
        end
    end

    if not targetNode.isShowing then
        show();
    end
end

-- 斥候物品飞入效果
function scoutItemsFlyTo(effectParent, fromPos, toPos, iconPath, num, callback)
    local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / speed;
    for i = 1, num do
        local icon = ccui.ImageView:create();
        icon:loadTexture(iconPath);
        icon:setPosition(fromPos);
        icon:setLocalZOrder(10000 - i);
        --icon:setVisible(false);
        icon:setScale(0.1);

        local delay = cc.DelayTime:create((i-1) * duration / 3);
        local show = cc.CallFunc:create(function()
            icon:setVisible(true);
            icon:setOpacity(255 - (i - 1) * 30);
        end);
        local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), 2.0);
        local rotate = cc.RotateBy:create(duration, 180);
        local scaleTo = cc.ScaleTo:create(duration, 0.8);
        local action = cc.Spawn:create(moveTo,rotate,scaleTo);

        local callfunc = cc.CallFunc:create(function()
            effectParent:removeChild(icon);
            if callback and i == num then
                callback();
            end
        end);

        icon:runAction(cc.Sequence:create(delay, show, action, callfunc));
        effectParent:addChild(icon);
    end

end

-- 物品飞入效果
function commonItemsFlyTo(effectParent, fromPos, toPos, iconPath, num, callback)
    local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / speed;

    num = iif(num > 3, 3, num);

    for i = 1, num do
        local icon = ccui.ImageView:create();
        icon:loadTexture(iconPath);
        icon:setPosition(fromPos);
        icon:setLocalZOrder(10000 - i);
        --icon:setVisible(false);

        local delay = cc.DelayTime:create((i-1) * duration / 3);
        local show = cc.CallFunc:create(function()
            icon:setVisible(true);
            icon:setOpacity(255 - (i - 1) * 30);
        end);
        local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), 2.0);
        local rotate = cc.RotateBy:create(duration, 180);
        local scaleTo = cc.ScaleTo:create(duration, 0.5);
        local action = cc.Spawn:create(moveTo,rotate,scaleTo);

        local callfunc = cc.CallFunc:create(function()
            effectParent:removeChild(icon);
            if callback and i == num then
                callback();
            end
        end);

        icon:runAction(cc.Sequence:create(delay, show, action, callfunc));
        effectParent:addChild(icon);
    end
end

-- 拾取卷轴特效
function pickupSpellEffect(sourceNode, targetNode, iconPath, itemId, num)
    if targetNode.gainItemList == nil then
        targetNode.gainItemList = {};
    end

    local name = ItemM.query(itemId, "name");
    for i = 1, num do
        table.insert(targetNode.gainItemList, name);
    end

    -- 目标位置先转成世界坐标
    local worldPos = targetNode:convertToWorldSpaceAR(cc.p(0, 0));

    -- 再转到相对于sourceNode的父节点的坐标
    local parentNode = sourceNode:getParent();
    local toPos = parentNode:convertToNodeSpace(worldPos);
    local fromPos = cc.p(sourceNode:getPosition());

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    local function callback()
        -- 卷轴图标闪一下
        playEffect(targetNode, 1106, 63, 53);

        local x = nil;
        local ap = cc.p(0.5, 0.5);

        if getLang() ~= "zh" and getLang() ~= "tw" then
            x = targetNode:getContentSize().width;
            ap = cc.p(1, 0.5);
        end

        showItemName(targetNode, TextStyleM.TEXT_COLOR_CYAN, x, nil, nil, ap);
    end

    commonItemsFlyTo(effectParent, fromPos, toPos, iconPath, num, callback);
end

-- 拾取道具特效
function pickupPropertyEffect(sourceNode, targetNode, iconPath, itemId, num)
    -- 目前拾取道具的特性和宝物相同，都是飞入到包裹中
    pickupEquipEffect(sourceNode, targetNode, iconPath, itemId, num);
end

-- 拾取宝物特效
function pickupEquipEffect(sourceNode, targetNode, iconPath, itemId, num)
    if targetNode.gainItemList == nil then
        targetNode.gainItemList = {};
    end

    local name = ItemM.query(itemId, "name");

    -- 奇怪的书籍名字动态获取
    if FormulaM.invoke("IS_STRANGE_BOOK", itemId) then
        name = CaveHideItemM.getStrangeBookName();
    end

    for i = 1, num do
        table.insert(targetNode.gainItemList, name);
    end

    -- 目标位置先转成世界坐标
    local worldPos = targetNode:convertToWorldSpaceAR(cc.p(0, 0));

    -- 再转到相对于sourceNode的父节点的坐标
    local parentNode = sourceNode:getParent();
    local toPos = parentNode:convertToNodeSpace(worldPos);
    local fromPos = cc.p(sourceNode:getPosition());

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    if tolua.type(parentNode) ~= "ccui.Layout" then
        toPos = effectParent:convertToNodeSpace(worldPos);
        fromPos = effectParent:convertToNodeSpace(sourceNode:convertToWorldSpaceAR(cc.p(0, 0)));
    end
    local function callback()
        -- 宝物闪一下
        playEffect(targetNode, 1107, 60, 58);
        --finishEffect();

        local x = nil;
        local ap = cc.p(0.5, 0.5);

        if getLang() ~= "zh" and getLang() ~= "tw" then
            x = 0;
            ap = cc.p(0, 0.5);
        end

        showItemName(targetNode, TextStyleM.TEXT_COLOR_CYAN, x, nil, nil, ap);
    end

    commonItemsFlyTo(effectParent, fromPos, toPos, iconPath, num, callback);
end

-- 喷泉光效
function fountainEffect(parentNode, adjustX, adjustY)
    playEffect(parentNode, 1109, adjustX-1, adjustY+13);
    playEffect(parentNode, 1110, adjustX, adjustY-7);
end

-- 播放蜂巢动画
function playBehiveEffect(sourceNode, targetNode, heroPos)
    -- 先隐藏格子上的蜜蜂
    local beeEffect = findChildByName(sourceNode.item, "bee_effect");
    if not beeEffect then
        return 0;
    end

    beeEffect:setVisible(false);

    if DungeonGuideM.isGuideFinished() then
        return behiveEffect(sourceNode, sourceNode, targetNode, heroPos, cc.p(0, 0));
    end

    -- 新手引导中，蜜蜂先飞到17格子盘旋一阵子，停在第19个格子，然后再开始攻击
    local firstGridNode = UIDungeonMgr.getCurLevel().grids[17];
    local secondGridNode = UIDungeonMgr.getCurLevel().grids[17];

    local fromPos = cc.p(sourceNode:getPosition());
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    local toPos = cc.p(firstGridNode:getPosition());

    -- 计算蜜蜂飞到目标怪物节点所需时间
    local duration = math.max(cc.pGetDistance(fromPos, toPos)) / speed;

    local node = cc.Node:create();
    effectParent:addChild(node);
    local _, effect = playEffect(node, 1138, 0, 0);
    node:setPosition(fromPos);

    local callfunc = cc.CallFunc:create(function()
        effect:setVisible(false);

        local function frameEventFunc(frame)
            if frame.getEvent and frame:getEvent() == "pilor" then
                local name = frame:getNode():getName();
                local index = frame:getFrameIndex();

                if index == 73 then
                    effect:setVisible(false);
                    behiveEffect(secondGridNode, sourceNode, targetNode, heroPos, cc.p(80, -20));
                end
            end
        end
        local _, effect = playEffect(node, 1228, 300, -20, frameEventFunc);
    end);

    local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), 2.0);
    node:runAction(cc.Sequence:create(moveTo, callfunc));

    -- 动作的执行时异步的，时间较难计算
    -- 因为是新手引导，直接把时间写死
    return 2.2;
end

-- 蜂巢轨迹
function behiveEffect(sourceNode, beeNode, targetNode, heroPos, offset)
    -- 获取不到出战宠物的位置
    if not heroPos then
        return 0;
    end

    -- 先隐藏格子上的蜜蜂
    local beeEffect = findChildByName(beeNode.item, "bee_effect");
    beeEffect:setVisible(false);

    -- 播放蜜蜂袭击轨迹
    local fromPos = cc.pAdd(cc.p(sourceNode:getPosition()), offset);
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();

    -- 计算蜜蜂飞到英雄节点所需时间
    local durationHero = math.max(cc.pGetDistance(fromPos, heroPos)) / speed;

    -- 触发出手事件的时机，默认取决于英雄
    local fireByHero = true;

    local duration = 0;
    if targetNode then
        local toPos = cc.p(targetNode:getPosition());

        -- 计算蜜蜂飞到目标怪物节点所需时间
        duration = math.max(cc.pGetDistance(fromPos, toPos)) / speed;

        -- 1.蜂巢攻击怪物
        local node = cc.Node:create();
        effectParent:addChild(node);
        local _, effect = playEffect(node, 1138, 0, 0);
        node:setPosition(fromPos);

        -- 如果怪物离蜂巢更远，则触发出手事件的时机取决于怪物
        if duration > durationHero then
            fireByHero = false;
        end

        local callfunc = cc.CallFunc:create(function()
            effectParent:removeChild(node);

            -- 目标爆炸
            playEffect(targetNode, 1153, 0, 0);
        end);

        local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), 2.0);
        node:runAction(cc.Sequence:create(moveTo, callfunc));
    end

    -- 2.蜂巢攻击玩家
    local function attackHero()
        local node2 = cc.Node:create();
        effectParent:addChild(node2);
        playEffect(node2, 1138, 0, 0);
        node2:setPosition(fromPos);

        local callfunc2 = cc.CallFunc:create(function()
            effectParent:removeChild(node2);

            local node3 = cc.Node:create();
            effectParent:addChild(node3);
            node3:setPosition(heroPos);

            -- 玩家受创光效
            playEffect(node3, 1153, 0, 0);

            -- 如果蜜蜂格子未失效
            if beeNode.gridData.state ~= GRID_STATE_DISABLE then
                -- 格子上的蜜蜂再次出现
                beeEffect:setVisible(true);
            end

            local function updateHp()
                -- 玩家掉血事件
                EventMgr.fire(event.USER_INFO_UPDATED, "hp");
            end

            -- 稍微延迟一下，等受创完毕再更新血量显示
            performWithDelay(sourceNode, updateHp, 1.0);
        end);

        if heroPos then
            local moveTo2 = cc.EaseInOut:create(cc.MoveTo:create(durationHero, heroPos), 2.0);
            node2:runAction(cc.Sequence:create(moveTo2, callfunc2));
        end
    end

    attackHero();
    return math.max(duration, durationHero);
end

-- 猎人陷阱轨迹
function beelineTrapEffect(sourceNode, targetNode, direction)
    local fromPos = cc.p(sourceNode:getPosition());
    print("fromPosX:"..fromPos.x..",fromPosY:"..fromPos.y);

    local toPos = cc.p(targetNode:getPosition());
    print("toPosX:"..toPos.x..",toPosY:"..toPos.y);

    -- 获取需要旋转的角度
    local function getRotateAngle()
        if direction == "up" then
            -- 上，不需要旋转
            return 0;
        elseif direction == "right" then
            -- 右
            return 90;
        elseif direction == "down" then
            -- 下
            return 180;
        elseif direction == "left" then
            -- 左
            return 270;
        else
            cclog("### 未知的方向(%s)", tostring(direction));
            return 0;
        end
    end

    local angle = getRotateAngle();

    trace("Effects", "弩车旋转角度：%d", angle);

    -- 隐藏格子上的箭
    local gridArowImg = findChildByName(sourceNode.item, "arow");
    if gridArowImg then
        gridArowImg:setVisible(false);
    end

    -- 陷阱启动特效
    sourceNode.animation:play(1);

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();

    -- 金属发光
    local nodeLight = cc.Node:create();
    local effectChild = findChildByName(effectParent, "trap_light");
    if effectChild then
        effectParent:removeChild(effectChild);
    end

    nodeLight:setName("trap_light");
    playEffect(nodeLight, 1155, 0, 0);
    effectParent:addChild(nodeLight);

    -- 根据方向修正启动光效的位置
    if direction == "up" then
        -- 上
        nodeLight:setPosition(fromPos.x, fromPos.y + 35);
    elseif direction == "right" then
        -- 右
        nodeLight:setPosition(fromPos.x + 35, fromPos.y);
    elseif direction == "down" then
        -- 下
        nodeLight:setPosition(fromPos.x, fromPos.y - 35);
    elseif direction == "left" then
        -- 左
        nodeLight:setPosition(fromPos.x - 35, fromPos.y);
    end

    -- 启动时间
    local prepareTime = 0.5;

    -- 启动时箭头往回拉伸的距离
    local offset = 10;

    -- 绘制箭头（需要飞出去）
    local arowImg = ccui.ImageView:create("images/ui/level/item/arow.png");

    -- 修正箭头的位置
    if direction == "up" then
        -- 上
        arowImg:setPosition(0, 15 + offset);
        local moveBy = cc.MoveBy:create(prepareTime/2, cc.p(0, -offset));
        arowImg:runAction(moveBy);
    elseif direction == "right" then
        -- 右
        arowImg:setPosition(-offset, 22);
        local moveBy = cc.MoveBy:create(prepareTime/2, cc.p(offset, 0));
        arowImg:runAction(moveBy);
    elseif direction == "down" then
        -- 下
        arowImg:setPosition(2, 15 - offset);
        local moveBy = cc.MoveBy:create(prepareTime/2, cc.p(0, offset));
        arowImg:runAction(moveBy);
    elseif direction == "left" then
        -- 左
        arowImg:setPosition(offset, 22);
        local moveBy = cc.MoveBy:create(prepareTime/2, cc.p(-offset, 0));
        arowImg:runAction(moveBy);
    end

    local nodeArow = cc.Node:create();
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    effectParent:addChild(nodeArow);
    nodeArow:setPosition(fromPos);
    nodeArow:addChild(arowImg);
    arowImg:setRotation(angle);

    -- 发射
    local function launch()
        local duration = math.max(cc.pGetDistance(fromPos, toPos)) / (2.5*speed);

        local callfunc = cc.CallFunc:create(function()
            effectParent:removeChild(nodeArow);

            -- 射中目标效果
            playEffect(targetNode, 1153, 0, 0);

            finishEffect();

            -- 触发出手事件，开始播放受创动作
            EventMgr.fire(event.PLAYER_MOVE, sourceNode.index);
        end);

        local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), 2.0);
        nodeArow:runAction(cc.Sequence:create(moveTo, callfunc));
    end

    performWithDelay(sourceNode, launch, prepareTime);
end

-- 土制陷阱（炮台）轨迹
function cropTrapEffect(sourceNode, targetNodes)
    sourceNode.icon:setVisible(false);
    local fromPos = cc.p(sourceNode:getPosition());

    local node = cc.Node:create();
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    effectParent:addChild(node);
    node:setPosition(fromPos);

    -- 炮台启动特效
    playEffect(node, 1140, -1, 15);

    -- 炮台启动时间
    local prepareTime = 0.5;

    -- 发射
    local function launch()
        if sourceNode.icon then
            sourceNode.icon:setVisible(true);
        end

        effectParent:removeChild(node);

        -- 获取炮弹的图标路径
        local bulletPath = getLevelItemIconPath("bullet");

        -- 子弹目的地偏移
        local function getOffset(direction)
            if direction == "left" then
                return { 80, 0 };
            elseif direction == "up" then
                return { 0, -60 };
            elseif direction == "right" then
                return { -70, 0 };
            elseif direction == "down" then
                return { 0, 120 };
            end
        end

        for direction, targetList in pairs(targetNodes) do

            -- 速度系数
            local speedCoef = 3.0;

            if #targetList > 0 then
                -- 绘制炮弹
                local bulletImg = ccui.ImageView:create(bulletPath);
                local node = cc.Node:create();
                local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
                effectParent:addChild(node);
                node:setPosition(fromPos);
                node:addChild(bulletImg);

                -- 旋转炮弹方向
                if direction == "right" then
                    -- 右，不需要旋转
                    bulletImg:setPosition(80, 35);
                elseif direction == "down" then
                    -- 下
                    bulletImg:setRotation(90);
                    bulletImg:setPosition(0, -80);
                elseif direction == "left" then
                    -- 左
                    bulletImg:setRotation(180);
                    bulletImg:setPosition(-80, 35);
                elseif direction == "up" then
                    -- 上
                    bulletImg:setRotation(270);
                    bulletImg:setPosition(0, 80);
                end

                -- 计算到达各个节点所需的时间
                local delayList = {};
                for _, targetNode in pairs(targetList) do
                    local toPos = cc.p(targetNode:getPosition());
                    local duration = cc.pGetDistance(fromPos, toPos)/(speedCoef*speed);
                    table.insert(delayList, duration);
                end

                -- 获取该方向上最远的目标
                local farmostNode = targetList[#targetList];
                local farmostPos = cc.p(farmostNode:getPosition());
                local duration = cc.pGetDistance(fromPos, farmostPos)/(speedCoef*speed);

                local callfunc = cc.CallFunc:create(function()
                    -- 移除子弹
                    effectParent:removeChild(node);
                    finishEffect();
                end);

                local moveTo = cc.MoveTo:create(duration, farmostPos);
                node:runAction(cc.Sequence:create(moveTo, callfunc));

                for i, delay in pairs(delayList) do
                    local targetNode = targetList[i];

                    -- 爆炸
                    local function playDamage()
                        --cclog("格子(%d)爆炸", targetNode.gridData:getPos());

                        playEffect(targetNode, 1006, 0, 0);
                        targetNode:playEffect(targetNode, "animation/open_block.csb", 0.5, nil, 1);
                    end

                    performWithDelay(sourceNode, playDamage, delay);
                end

                -- 抛出出手事件
                EventMgr.fire(event.PLAYER_MOVE, { ["combatRound"] = true, });
            end
        end
    end

    performWithDelay(sourceNode, launch, prepareTime);
end

-- 破魔重弩轨迹
function crossbowTrapEffect(sourceNode, targetNode)
    local fromPos = cc.p(sourceNode:getPosition());
    print("fromPosX:"..fromPos.x..",fromPosY:"..fromPos.y);

    local toPos = cc.p(targetNode:getPosition());
    print("toPosX:"..toPos.x..",toPosY:"..toPos.y);

    -- 隐藏格子上的弓
    local gridArowImg = findChildByName(sourceNode.item, "arow");
    gridArowImg:setVisible(false);

    -- 陷阱启动特效
    sourceNode.animation:play(1);

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();

    -- 金属发光
    local nodeLight = cc.Node:create();
    local effectChild = findChildByName(effectParent, "trap_light");
    if effectChild then
        effectParent:removeChild(effectChild);
    end

    nodeLight:setName("trap_light");
    playEffect(nodeLight, 1155, 0, 0);
    effectParent:addChild(nodeLight);

    -- 根据方向修正启动光效的位置
    nodeLight:setPosition(fromPos.x, fromPos.y + 35);

    -- 启动时间
    local prepareTime = 1.5;

    -- 发射
    local function launch()
        effectParent:removeChild(nodeArow);

        -- 射中目标效果、音效
        AudioM.playFx("tiro_crossbow_hit");
        playEffect(targetNode, 1153, 0, 0);

        finishEffect();

        -- 触发出手事件，开始播放受创动作
        EventMgr.fire(event.PLAYER_MOVE, sourceNode.index);
    end

    performWithDelay(sourceNode, launch, prepareTime);
end

-- 血瓶轨迹
function gainHpEffect(sourceNode, iconNode, textNode, addValue)
    playEffect(sourceNode, 1115, 0, 20);

    local originToPosX = iconNode:getPositionX();
    local originToPosY = iconNode:getPositionY();

    -- 目标位置先转成世界坐标
    local worldPos = iconNode:convertToWorldSpaceAR(cc.p(0, 40));
    --print("originToPosX:"..originToPosY..",toPosY:"..originToPosY..", world posx:"..worldPos.x..",posy:"..worldPos.y);

    -- 再转到相对于sourceNode的父节点的坐标
    local parentNode = sourceNode:getParent();
    local toPos = parentNode:convertToNodeSpace(worldPos);
    --print("toPosX:"..toPos.x..",toPosY:"..toPos.y);

    local fromPos = cc.p(sourceNode:getPosition());
    --print("fromPosX:"..fromPos.x..",fromPosY:"..fromPos.y);

    local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / speed;

    local angle = cc.pGetAngle(fromPos, toPos);
    angle = radiansToDegrees(angle);
    --print("angle:"..angle);

    local node = cc.Node:create();
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    effectParent:addChild(node);
    local _, effect = playEffect(node, 1116, 0, 0);
    node:setPosition(fromPos);
    node:setRotation(angle);

    local callfunc = cc.CallFunc:create(function()
        effectParent:removeChild(node);

        -- 界面闪一下
        playEffect(iconNode, 1117, 35, 27);
        commonTextEffect(textNode, TextStyleM.TEXT_COLOR_LIGHT_YELLOW);

        finishEffect("gain_hp");
    end);

    local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), 2.0);
    node:runAction(cc.Sequence:create( moveTo, callfunc));
end

-- 蓝瓶轨迹
function gainMpEffect(sourceNode, iconNode, textNode, addValue)
    playEffect(sourceNode, 1118, 0, 20);

    local originToPosX = iconNode:getPositionX();
    local originToPosY = iconNode:getPositionY();

    -- 目标位置先转成世界坐标
    local worldPos = iconNode:convertToWorldSpaceAR(cc.p(0, 40));
    --print("originToPosX:"..originToPosY..",toPosY:"..originToPosY..", world posx:"..worldPos.x..",posy:"..worldPos.y);

    -- 再转到相对于sourceNode的父节点的坐标
    local parentNode = sourceNode:getParent();
    local toPos = parentNode:convertToNodeSpace(worldPos);
    --print("toPosX:"..toPos.x..",toPosY:"..toPos.y);

    local fromPos = cc.p(sourceNode:getPosition());
    --print("fromPosX:"..fromPos.x..",fromPosY:"..fromPos.y);

    local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / speed;

    local angle = cc.pGetAngle(fromPos, toPos);
    angle = radiansToDegrees(angle);
    --print("angle:"..angle);

    local node = cc.Node:create();
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    effectParent:addChild(node);
    local _, effect = playEffect(node, 1119, 0, 0);
    node:setPosition(fromPos);
    node:setRotation(angle+20);

    local callfunc = cc.CallFunc:create(function()
        effectParent:removeChild(node);

        -- 界面闪一下
        playEffect(iconNode, 1120, 25, 27);
        commonTextEffect(textNode, TextStyleM.TEXT_COLOR_LIGHT_YELLOW);

        finishEffect("gain_mp");
    end);

    local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), 2.0);
    node:runAction(cc.Sequence:create( moveTo, callfunc));
end

-- 攻击轨迹
function gainAttackEffect(sourceNode, iconNode, textNode, addValue)
    playEffect(sourceNode, 1441);

    local originToPosX = iconNode:getPositionX();
    local originToPosY = iconNode:getPositionY();

    -- 目标位置先转成世界坐标
    local worldPos = iconNode:convertToWorldSpaceAR(cc.p(0, 40));
    --print("originToPosX:"..originToPosY..",toPosY:"..originToPosY..", world posx:"..worldPos.x..",posy:"..worldPos.y);

    -- 再转到相对于sourceNode的父节点的坐标
    local parentNode = sourceNode:getParent();
    local toPos = parentNode:convertToNodeSpace(worldPos);
    --print("toPosX:"..toPos.x..",toPosY:"..toPos.y);

    local fromPos = cc.p(sourceNode:getPosition());
    --print("fromPosX:"..fromPos.x..",fromPosY:"..fromPos.y);

    local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / speed;

    local angle = cc.pGetAngle(fromPos, toPos);
    angle = radiansToDegrees(angle);
    --print("angle:"..angle);

    local node = cc.Node:create();
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    effectParent:addChild(node);
    local _, effect = playEffect(node, 1142);
    node:setPosition(fromPos);
    node:setRotation(angle);

    local callfunc = cc.CallFunc:create(function()
        effectParent:removeChild(node);

        -- 界面闪一下
        playEffect(iconNode, 1143, 30, 40);
        commonTextEffect(textNode, TextStyleM.TEXT_COLOR_LIGHT_YELLOW);

        finishEffect("gain_attack");
    end);

    local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), 2.0);
    node:runAction(cc.Sequence:create( moveTo, callfunc));
end

-- 魔力轨迹
function gainMagicEffect(sourceNode, iconNode, textNode, addValue)
    playEffect(sourceNode, 1144);

    local originToPosX = iconNode:getPositionX();
    local originToPosY = iconNode:getPositionY();

    -- 目标位置先转成世界坐标
    local worldPos = iconNode:convertToWorldSpaceAR(cc.p(0, 40));
    --print("originToPosX:"..originToPosY..",toPosY:"..originToPosY..", world posx:"..worldPos.x..",posy:"..worldPos.y);

    -- 再转到相对于sourceNode的父节点的坐标
    local parentNode = sourceNode:getParent();
    local toPos = parentNode:convertToNodeSpace(worldPos);
    --print("toPosX:"..toPos.x..",toPosY:"..toPos.y);

    local fromPos = cc.p(sourceNode:getPosition());
    --print("fromPosX:"..fromPos.x..",fromPosY:"..fromPos.y);

    local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / speed;

    local angle = cc.pGetAngle(fromPos, toPos);
    angle = radiansToDegrees(angle);
    --print("angle:"..angle);

    local node = cc.Node:create();
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    effectParent:addChild(node);
    local _, effect = playEffect(node, 1145);
    node:setPosition(fromPos);
    node:setRotation(angle+20);

    local callfunc = cc.CallFunc:create(function()
        effectParent:removeChild(node);

        -- 界面闪一下
        playEffect(iconNode, 1146, 30, 25);
        commonTextEffect(textNode, TextStyleM.TEXT_COLOR_LIGHT_YELLOW);

        finishEffect("gain_magic");
    end);

    local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), 2.0);
    node:runAction(cc.Sequence:create( moveTo, callfunc));
end

-- 拾取龙珠特效
function pickDragonBallEffect(sourceNode, targetNode, iconPath, sourcePosOffset, targetPosOffset)
    if targetNode == nil then
        return;
    end

    -- 设置表现层龙珠数量
    local uiDungeonMain = UIMgr:getCurrentScene():getFormByName("UIDungeonMain");
    if uiDungeonMain then
        local curUINum = uiDungeonMain.dungeonTopMenu:getCurUIBallCount();
        uiDungeonMain.dungeonTopMenu:setCurUIBallCount(curUINum + 1);
    end

    playEffect(sourceNode, 1121, 0, 20);

    local originToPosX = targetNode:getPositionX();
    local originToPosY = targetNode:getPositionY();

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();

    -- 目标位置先转成世界坐标
    local worldPos = targetNode:convertToWorldSpaceAR(cc.p(0, 0));

    -- 再转换到effectParent所在的坐标系
    local toPos = effectParent:convertToNodeSpace(worldPos);
    toPos = targetPosOffset and cc.pAdd(cc.p(toPos.x, toPos.y), targetPosOffset) or toPos;

    sourcePosOffset = sourcePosOffset and sourcePosOffset or cc.p(0, 0);
    local fromPos = cc.pAdd(cc.p(sourceNode:getPositionX(), sourceNode:getPositionY()), sourcePosOffset);
    worldPos = sourceNode:convertToWorldSpace(cc.p(0, 0));
    fromPos = effectParent:convertToNodeSpace(worldPos);

    local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / speed;

    local icon = ccui.ImageView:create();
    icon:loadTexture(iconPath);
    icon:setPosition(fromPos);
    icon:setLocalZOrder(10000);

    local callfunc = cc.CallFunc:create(function()
        local curIconScale = icon:getScale();
        local scaleUp = cc.ScaleTo:create(0.5, curIconScale * 1.2);
        local remove = cc.CallFunc:create(function()
            effectParent:removeChild(icon);
        end);

        icon:runAction(cc.Sequence:create(cc.Spawn:create(scaleUp, cc.FadeOut:create(0.5)), remove));

        targetNode:setVisible(true);

        -- 龙珠图标闪一下
        local bgNode = UIDungeonMgr.getCurLevel().dungeonTopMenu:getDragonBallBgNode();
        local bgBackNode = UIDungeonMgr.getCurLevel().dungeonTopMenu:getDragonBallBgBackNode();

        playEffect(bgNode, 1160, 125, 45);

        local function play2()
            playEffect(bgBackNode, 1159, 7, 6);
        end
        performWithDelay(bgBackNode, play2, 0.5);

        finishEffect("dragon_ball");
    end);

    local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), 2.0);
    local scaleTo = cc.ScaleTo:create(duration, 0.2);
    icon:runAction(cc.Sequence:create( cc.Spawn:create(moveTo, scaleTo), callfunc));
    effectParent:addChild(icon);
end

-- 获取龙珠的飞入特效
function gainDragonBallEffect(sourceNode, num)
    if type(num) ~= "number" then
        return;
    end

    AudioM.playFx("pick_goods");

    -- 分批次飞入
    local PICK_INTERVALE = 0.5;
    for i = 1, num do

        local function callback2()
            -- 获取表现层龙珠数量
            local curUINum = 0;
            local uiDungeonMain = UIMgr:getCurrentScene():getFormByName("UIDungeonMain");
            if uiDungeonMain then
                curUINum = uiDungeonMain.dungeonTopMenu:getCurUIBallCount();
            end

            if curUINum > 7 then
                curUINum = 7;
            end

            local wishItemIcons = { 1500, 1501, 1502, 1503, 1504, 1505, 1506, };
            local iconIndex = curUINum == 0 and 1 or curUINum;
            iconPath = getItemIconPath(wishItemIcons[iconIndex]);
            pickDragonBallEffect(sourceNode, UIDungeonMgr.getCurLevel().dungeonTopMenu:getDragonBallNode(), iconPath);
        end

        performWithDelay(sourceNode, callback2, PICK_INTERVALE*(i - 1));
    end
end

-- 稀有物品轨迹
function gainSpecialItemEffect(sourceNode, targetNode, iconPath, textNode, total, delta, extra)
    playEffect(sourceNode, 1123, 0, 20);

    local originToPosX = targetNode:getPositionX();
    local originToPosY = targetNode:getPositionY();

    local fromPosOffset;

    -- 位置偏移
    if extra and extra["fromPosOffset"] then
        fromPosOffset = extra["fromPosOffset"];
    end

    -- 之前的值
    textNode:setString(tonumber(total - delta));

    -- 边距
    local margin = 20;

    -- 文本长度
    local textWidth = textNode:getContentSize().width;
    -- 图标+间隙的宽度
    local extraWidth = 30;

    -- 重新设置text的坐标
    textNode:setPositionX(400 - textWidth - margin);
    -- 重新设置icon的坐标
    targetNode:setPositionX(textNode:getPositionX() - extraWidth);

    -- 需要伸出去的长度
    local xDistance = textWidth + extraWidth + margin * 2;

    local curScale;

    -- 目标位置先转成世界坐标
    local barNode = targetNode:getParent();
    local targetIconPath = iconPath;
    if iconPath == getSmallAttribIconPath("money") then
        targetIconPath = getLevelItemIconPath("gold_small");
        curScale = 0.5;
    elseif iconPath == getSmallAttribIconPath("gem") then
        targetIconPath = getLevelItemIconPath("gem");
        curScale = 0.35;
    else
        curScale = 0.4;
    end

    local pos = barNode:getParent():convertToNodeSpace(cc.p(0, 0));

    targetNode:setScale(curScale);
    targetNode:loadTexture(targetIconPath);
    barNode:setPositionX(pos.x + xDistance); -- 先预设到目标位置，方便求出物品飞到的目的地世界坐标
    local worldPos = targetNode:convertToWorldSpaceAR(cc.p(0, 0));

    -- 条弹出
    barNode:setPositionX(-200);
    barNode:setVisible(true);
    local barMoveTo = cc.MoveTo:create(0.6, cc.p(pos.x + xDistance, barNode:getPositionY()));
    barNode:stopAllActions();
    barNode:runAction(barMoveTo);


    -- 再转到相对于sourceNode的父节点的坐标
    local parentNode = sourceNode:getParent();
    local toPos = parentNode:convertToNodeSpace(worldPos);
    --print("toPosX:"..toPos.x..",toPosY:"..toPos.y);

    local fromPos = cc.p(sourceNode:getPosition());
    --print("fromPosX:"..fromPos.x..",fromPosY:"..fromPos.y);

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / speed;

    --[[ 这里是一个比较恶心的处理，目前没有比较好的方案去解决它，先这样处理。
    理论上来说toPos和fromPos应该是要根据 元素(icon)的父节点(effectParent)来计算父节点中的对应坐标的
    这个计算过程应该是先将目标位置先转成世界坐标，再转到相对于effectParent节点的坐标
    但是如果sourceNode是迷宫中的格子，那么 将目标位置先转成世界坐标 这个操作将会出现问题
    将目标位置先转成世界坐标 其实是将目前位置的坐标加上父节点的坐标
    迷宫中的格子的父节点是个容器，容器中元素的坐标是相对于容器的左下角的坐标，而不是容器描点的坐标。
    使得"将目前位置的坐标加上父节点的坐标"这个计算过程不适用于容器
    因此将迷宫中的格子的父节点这个容器设置为跟effectParent节点完全一样的容器，然后直接使用其父节点来计算坐标
    当sourceNode的父节点不是一个容器时，就使用effectParent节点的对应坐标来处理
    ]]
    if tolua.type(parentNode) ~= "ccui.Layout" then
        toPos = effectParent:convertToNodeSpace(worldPos);
        fromPos = effectParent:convertToNodeSpace(sourceNode:convertToWorldSpaceAR(cc.p(0, 0)));
    elseif parentNode.spaceChange then
        toPos = effectParent:convertToNodeSpace(worldPos);
    end

    if fromPosOffset then
        fromPos = cc.pAdd(fromPos, cc.p(fromPosOffset[1], fromPosOffset[2]));
    end

    local icon = ccui.ImageView:create();
    if iconPath == getSmallAttribIconPath("money") then
        iconPath = getLevelItemIconPath("gold_small");
    end
    icon:loadTexture(iconPath);
    icon:setPosition(fromPos);
    icon:setLocalZOrder(10000);

    local callfunc = cc.CallFunc:create(function()
        local curIconScale = icon:getScale();
        local scaleUp = cc.ScaleTo:create(0.5, curIconScale * 1.3);
        local remove = cc.CallFunc:create(function()
            effectParent:removeChild(icon);
        end);

        icon:runAction(cc.Sequence:create(cc.Spawn:create(scaleUp, cc.FadeOut:create(0.5)), remove));

        -- 改变后的值
        textNode:setString(tostring(total));

        -- 文字变大特效
        commonTextEffect(textNode);

        -- 绿色的+N数字上飘
        local fontSize = 20;
        local text = TextPoolM.createTextObject(fontSize);
        text:setString("+"..delta);
        text:setTextColor(TextStyleM.TEXT_COLOR_LIGHT_GREEN);
        text:setPosition(0, -20);
        text:setAnchorPoint(cc.p(0, 0));
        local callfunc = cc.CallFunc:create(function()
            textNode:removeChild(text);
            TextPoolM.freeTextObject(text);
        end);
        textNode:addChild(text);

        local moveBy = cc.MoveBy:create(1, cc.p(0, 50));
        local delay = cc.DelayTime:create(0.5);
        local fadeOut = cc.FadeOut:create(0.5);
        local action = cc.Spawn:create(moveBy, cc.Sequence:create(delay, fadeOut));
        text:runAction(cc.Sequence:create(action, callfunc));

        -- 物品栏缩回去
        local delay = cc.DelayTime:create(1.5);
        local barMoveBack = cc.MoveTo:create(0.2, cc.p(-200, barNode:getPositionY()));
        local backOk = cc.CallFunc:create(function()
            barNode.isBusy = false;
            barNode:setVisible(false);
        end);
        barNode:runAction(cc.Sequence:create(delay, barMoveBack, backOk));
        -- local curScale = targetNode:getScale();
        targetNode:runAction(cc.Sequence:create(cc.ScaleTo:create(0.3, curScale * 1.2), cc.ScaleTo:create(0.3, curScale * 1.0)));

        finishEffect();
    end);

    local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), 2.0);
    local rotate = cc.RotateBy:create(duration, 360);
    local scaleTo = cc.ScaleTo:create(duration, curScale);
    icon:runAction(cc.Sequence:create( cc.Spawn:create(moveTo, rotate, scaleTo), callfunc));
    effectParent:addChild(icon);
end

local labelList = {};

-- 去掉升级动画节点
function removeLevelUpEffect(parentNode)
    -- 去掉原本的节点
    parentNode:stopAllActions();
    if #labelList ~= 0 then
        for i = 1, #labelList do
            local label = labelList[i];
            parentNode:removeChild(label);
        end
    end
    labelList = {};
end

-- 显示升级动画
function playLevelUpEffect(parentNode, level, func, stopAction)
    if not stopAction then
        playEffect(parentNode, 1126, -65, 0);
    end

    local str = getLocStr("level_up2");
    local strList = UserM.getLevelUpStringList(level);
    local msg = string.format(getLocStr("world_tree_level"), level);
    local moneyLimit = string.format(getLocStr("money_limit_change"), 100000);
    strList = table.append({ str, msg }, strList);
    strList = table.append(strList, { moneyLimit });

    parentNode:stopAllActions();

    local kerning = 0;
    if getLang() == "fr" then
        kerning = -2;
    end

    -- 去掉原本的节点
    if #labelList ~= 0 then
        for i = 1, #labelList do
            local label = labelList[i];
            parentNode:removeChild(label);
        end
    end

    labelList = {};
    for i = 1, #strList do
        local text = ccui.Text:create();
        text:getVirtualRenderer():setHorizontalAlignment(cc.TEXT_ALIGNMENT_CENTER);
        TextStyleM.setTextStyle(text, TextStyleM.TEXT_SIZE_MIDDLE, TextStyleM.TEXT_COLOR_BROWN_YELLOW, true, kerning, nil, nil, nil, nil, TextStyleM.TEXT_SIZE_MIDDLE);
        text:setString(strList[i]);
        text:setFontName(getLocFontName());
        text:setOpacity(0);
        text:setPositionY(-10);
        table.insert(labelList, text);
        parentNode:addChild(text);
    end

    -- 暂停一段时候，和动画配合出现
    local delay = cc.DelayTime:create(0.45);
    local callback = cc.CallFunc:create(function()
        -- 标题快速上升
        local num = #labelList;
        local label = labelList[1];
        TextStyleM.setTextStyle(label, TextStyleM.TEXT_SIZE_HUGE, TextStyleM.TEXT_COLOR_BROWN_YELLOW, true, kerning, nil, nil, nil, nil, nil, TextStyleM.getTextScale(str, 14));
        local move = cc.EaseElasticOut:create(cc.MoveTo:create(2.5, cc.p(0, num * 35)), 0.5);
        label:runAction(cc.Sequence:create(cc.FadeIn:create(0.1), move));
    end);

    local delay2 = cc.DelayTime:create(0.1);
    local callback2 = cc.CallFunc:create(function()
        -- 属性提升出现
        local num = #labelList;
        for i = 2, num do
            local label = labelList[i];
            label:setPositionY((num - i) * 35 - 50);
            local labelDelay = cc.DelayTime:create( i * 0.32 );
            local move = cc.MoveTo:create(0.35, cc.p(0, (num - i) * 35));
            label:runAction(cc.Sequence:create(labelDelay, cc.FadeIn:create(0.1), move));
        end

        -- 标题跳动
        local title = labelList[1];
        local jump = cc.JumpBy:create(1, cc.p(0, 0), 10, 1);
        title:runAction(cc.RepeatForever:create(jump));
    end);

    -- 直接显示到位
    local function showAll()
        local num = #labelList
        for i = 1, num do
            local label = labelList[i];
            label:stopAllActions();
            if i == 1 then
                label:setPosition(cc.p(0, num * 35));
                TextStyleM.setTextStyle(label, TextStyleM.TEXT_SIZE_HUGE, TextStyleM.TEXT_COLOR_BROWN_YELLOW, true, kerning, nil, nil, nil, nil, nil, TextStyleM.getTextScale(str, 14));
            else
                label:setPosition(cc.p(0, (num - i) * 35));
            end
            label:setOpacity(255);
        end
        -- 标题跳动
        local title = labelList[1];
        local jump = cc.JumpBy:create(1, cc.p(0, 0), 10, 1);
        title:runAction(cc.RepeatForever:create(jump));
        func();
    end

    if stopAction then
        showAll();
    else
        parentNode:runAction(cc.Sequence:create(delay, callback, delay2, callback2));
    end
end

-- 称号可学习提示效果
function playLearnSkillHintEffect(parentNode, adjustX, adjustY)
    parentNode:removeAllChildren();
    playEffect(parentNode, 1129, adjustX, adjustY);
end

-- 英雄界面可装备武器/徽章提示效果
function playCanEquipHintEffect(parentNode, adjustX, adjustY)
    parentNode:removeAllChildren();
    playEffect(parentNode, 1283, adjustX, adjustY);
end

-- 进阶宠物可以穿着装备效果
function playUpgradeHeroEquipEffect(parentNode, adjustX, adjustY)
    parentNode:removeAllChildren();
    playEffect(parentNode, 2053, adjustX, adjustY);
end

-- 升级称号特效
function playUpgradeSkillEffect(iconNode, starNode, callback)
    -- 星星特效先播
    local callFunc = cc.CallFunc:create(function()
        playEffect(starNode, 1131, 20, 25);
    end);

    -- 停一段事件
    local delay = cc.DelayTime:create(0.2);
    local callFunc2 = cc.CallFunc:create(function()
        -- 播放金属闪过的光标
        playEffect(iconNode, 1130, 60, 60);
    end);

    local delay2 = cc.DelayTime:create(0.5);
    local callFunc3 = cc.CallFunc:create(function()
        if callback then
            callback();
        end
    end);

    iconNode:runAction(cc.Sequence:create(callFunc, delay, callFunc2, delay2, callFunc3));
end

-- 属性闪光特效
function playAttribSparkEffect(parent, effectId)
    parent:removeAllChildren();
    local posX = -130;
    for i = 1, 6 do
        posX = posX + 35;
        local effectNode = cc.Node:create();
        effectNode:setPosition(posX, 0);
        parent:addChild(effectNode);

        local delay = math.random(1, 10);

        local function callback()
            playEffect(effectNode, 1133);
        end

        performWithDelay(effectNode, callback, delay*0.1)
    end
end

-- 武器徽章穿戴特效
function playEquipWeaonAndMedalEffect(iconNode, classId, callback)
    local iconIndex = ItemM.query(classId, "icon");
    local icon = ccui.ImageView:create();
    icon:loadTexture(getItemIconPath(iconIndex));
    icon:setPosition(cc.p(45, 45+40));

    icon:setScale(1.3);
    icon:setOpacity(0);
    iconNode:addChild(icon);

    local fadeIn = cc.FadeIn:create(0.3);
    local scaleTo = cc.EaseIn:create(cc.ScaleTo:create(0.3, 1), 5.5);
    local moveTo = cc.EaseIn:create(cc.MoveBy:create(0.3, cc.p(0, -40)), 5.5);
    local callFunc = cc.CallFunc:create(function()
        if callback then
            callback();
        end

        playEffect(iconNode, 1186, 45, 45);

        local curIconScale = icon:getScale();
        local scaleUp = cc.ScaleTo:create(0.5, curIconScale * 1.3);
        local remove = cc.CallFunc:create(function()
            iconNode:removeChild(icon);
        end);

        icon:runAction(cc.Sequence:create(cc.Spawn:create(scaleUp, cc.FadeOut:create(0.5)), remove));
    end);

    icon:runAction(cc.Sequence:create(cc.Spawn:create(fadeIn, scaleTo, moveTo), callFunc));
end

-- 拾取建筑奖励特效
function buildingBonusEffect(parent, iconPath)
    -- 格子渐隐消失
    if iconPath then
        local image = ccui.ImageView:create();
        image:loadTexture(iconPath);
        parent:addChild(image);

        local fadeOut = cc.FadeOut:create(1);
        image:runAction(fadeOut);
    end

    -- 播放光效
    playEffect(parent, 1134);
end

-- 窗口中间部分，弹性弹出
function playPopupEffect(node)
    local curScale = node:getScale();

    node:setScale(0.9 * curScale);
    local scaleUp1 = cc.EaseIn:create(cc.ScaleTo:create(0.2, 1.03 * curScale), 0.4);
    local scaleUp2 = cc.ScaleTo:create(0.05, 1 * curScale);
    node:runAction(cc.Sequence:create(scaleUp1, scaleUp2));
end

-- 渐隐消失
function playFadeOutEffect(node)
    node:stopAllActions();
    node:runAction(cc.Sequence:create(cc.FadeOut:create(0.1), cc.CallFunc:create(function() node:setVisible(false) end)));
end

-- 渐渐显示
function playFadeInEffect(node)
    if not node:isVisible() then
        node:setVisible(true);
    end

    node:stopAllActions();
    node:setOpacity(0);
    node:runAction(cc.FadeIn:create(0.1));
end

-----------------------------------
-- 播放气泡动画
-- @param parentNode  气泡节点
-- @param isBgFade    是否播放背景淡入淡出动画
-- @param originScale   初始缩放
function playBubbleAnima(parentNode, isBgFade, originScale, iScale, tScale, fScale, speed)
    if originScale == nil then
        originScale = 1;
    end

    -- 重复动画相关配置
    local initScale  = iScale or 0.9 * originScale;    -- 初始模型比例
    local transScale = tScale or 0.95 * originScale;   -- 过渡模型比例
    local finalScale = fScale or 1.0 * originScale;    -- 最终模型比例
    local speedCoef =  speed or 1.0;                  -- 速度系数

    -- 默认需要播放背景框淡入、淡出动画
    if isBgFade == nil then
        isBgFade = true;
    end

    -- 播放重复动画
    local function playRepeatAnima()
        -- 初始化
        parentNode:setVisible(true);
        parentNode:stopAllActions();
        parentNode:setScale(initScale);

        -- 开始重复动画
        -- 1.变大
        local upTime    = 0.25;
        local up2Time   = 0.25/speedCoef;
        local scaleUp   = cc.ScaleTo:create(upTime, transScale);
        local scaleUp2  = cc.ScaleTo:create(up2Time, finalScale);

        local seq1 = cc.Sequence:create(scaleUp, scaleUp2);
        local action1 = cc.Spawn:create(seq1);

        -- 2.短暂停顿
        local delayTime = 0.2/speedCoef;
        local delay = cc.DelayTime:create(delayTime);

        -- 3.变小
        local downTime  = 0.25/speedCoef;
        local down2Time = 0.25/speedCoef;
        local scaleDown = cc.ScaleTo:create(downTime, transScale);
        local scaleDown2 = cc.ScaleTo:create(down2Time, initScale);
        local seq2 = cc.Sequence:create(scaleDown, scaleDown2);
        local action2 = cc.Spawn:create(seq2);

        local sequenceNodeAction = cc.Sequence:create(action1, delay, action2);
        parentNode:runAction(cc.RepeatForever:create(sequenceNodeAction));

        -- 背景框淡入、淡出
        if isBgFade then
            local bgDialog = findChildByName(parentNode, "bg");
            local inTime  = upTime + up2Time + delayTime;   -- 淡入时间
            local outTime = downTime + down2Time;           -- 淡出时间
            local fadeIn = cc.FadeIn:create(outTime);
            local fadeOut = cc.FadeTo:create(outTime, 150);
            local sequenceBgAction = cc.Sequence:create(fadeIn, fadeOut);
            bgDialog:runAction(cc.RepeatForever:create(sequenceBgAction));
        end
    end
    playRepeatAnima();
end

-----------------------------------
-- 播放气泡轻微的呼吸动画
-- @param parentNode  气泡节点
-- @param amp         幅度
function playBubbleSlightAnima(parentNode, amp, speed)
    if amp == nil then
        amp = 0.01;
    end

    local playTime = speed or 0.5;

    -- 气泡轻微收缩
    local curScale = parentNode:getScale();
    local scaleDown = cc.ScaleTo:create(playTime, curScale * (1 - amp));
    local scaleUp = cc.ScaleTo:create(playTime, curScale);
    local delay = cc.DelayTime:create(0.2);
    local action = cc.Sequence:create(scaleDown, scaleUp, delay);

    parentNode:runAction(cc.RepeatForever:create(action));
end

-----------------------------------
-- 播放静态图片的呼吸动画
-- @param parentNode 节点
function playSlightAnima(parentNode, originScale, extraPara)
    originScale = originScale or 1;

    extraPara = extraPara or {};
    -- 固定呼吸的幅度，从0.95至1
    -- 将锚点设置在底部居中，方便播放动画
    if not extraPara["noChange"] then
        parentNode:setAnchorPoint(cc.p(0.5, 0));
        parentNode:setPosition(parentNode:getPositionX(),
                          parentNode:getPositionY() - parentNode:getContentSize().height / 2 * originScale);
    end

    local function changeScale()
        local function doChange()
            local scaleUp = cc.ScaleTo:create(1, 1 * originScale, 1 * originScale);
            local scaleDown = cc.ScaleTo:create(1, 1 * originScale, 0.95 * originScale);
            local function changeScalDone()
                if parentNode then
                    performWithDelay(parentNode, doChange, 0.2);
                end
            end

            local callFunc = cc.CallFunc:create(changeScalDone);
            parentNode:runAction(cc.Sequence:create(scaleUp, scaleDown, callFunc));
        end

        if parentNode then
            performWithDelay(parentNode, doChange, 0.1);
        end
    end

    if parentNode then
        performWithDelay(parentNode, changeScale, 0.1);
    end
end

-------------------------------
-- 播放上下浮动的动画
function playFluctuateAnima(node, duration, offset)
    local moveDown = cc.MoveBy:create(duration, cc.p(0, -offset));
    local moveUp = cc.MoveBy:create(duration, cc.p(0, offset));

    local action = cc.Sequence:create(moveDown, moveUp);
    local fluctuateAction = node:runAction(cc.RepeatForever:create(action));
    node.fluctuateAction = fluctuateAction;
end

-------------------------------
-- 播放上下浮动的动画,带一点变形
function playFluctuateAnima2(node, duration, offset)
    -- 下移
    local moveDown = cc.MoveBy:create(duration, cc.p(0, -offset));
    -- 上移
    local moveUp = cc.MoveBy:create(duration, cc.p(0, offset));
    local oriScaleX = node:getScaleX();
    local oriScaleY = node:getScaleY();
    -- 压缩
    local compressDown = cc.ScaleTo:create(duration, oriScaleX, oriScaleY-0.03);
    -- 拉伸
    local stretchUp = cc.ScaleTo:create(duration, oriScaleX, oriScaleY);

    local action = cc.Sequence:create(moveDown, moveUp);
    local action2 = cc.Sequence:create(compressDown, stretchUp);
    node:runAction(cc.RepeatForever:create(action));
    node:runAction(cc.RepeatForever:create(action2));
end

-- 魔藤技能升级特效
function playBineSkillUpgradeEffect(iconNode, starNode, callback, starOffsetX, starOffsetY)
    -- 星星特效先播
    local callFunc = cc.CallFunc:create(function()
        starOffsetX = starOffsetX or 20;
        starOffsetY = starOffsetY or 25;
        playEffect(starNode, 1131, starOffsetX, starOffsetY);
    end);

    -- 停一段事件
    local delay = cc.DelayTime:create(0.2);
    local callFunc2 = cc.CallFunc:create(function()
        -- 播放金属闪过的光标
        playEffect(iconNode, 1186, 45, 45);
    end);

    local delay2 = cc.DelayTime:create(0.5);
    local callFunc3 = cc.CallFunc:create(function()
        if callback then
            callback();
        end
    end);

    iconNode:runAction(cc.Sequence:create(callFunc, delay, callFunc2, delay2, callFunc3));
end

-- 播放魔藤攻击动画
function playBineAttack(sourceNode, targetNode)
    local offsetY = 60;  -- 充能光效位置在格子正上方
    local fromPos = cc.p(sourceNode:getPositionX(), sourceNode:getPositionY() + offsetY);
    local toPos   = cc.p(targetNode:getPosition());

    trace("Effect", "[playBineAttack]fromPosX = %d, fromPosY = %d", fromPos.x, fromPos.y);
    trace("Effect", "[playBineAttack]toPosX = %d, toPosY = %d", toPos.x, toPos.y);

    -- 1.播放魔藤充能特效
    playEffect(sourceNode, 3004, 160, 0);

    -- 2.创建射线轨迹

    local deltalX = toPos.x - fromPos.x;
    local deltalY = toPos.y - fromPos.y;

    -- 计算两点间的距离
    local distance = math.sqrt(math.pow(deltalX, 2) + math.pow(deltalY, 2));

    -- 计算旋转角度
    local function calcAngle()
        local ang;
        if toPos.y > fromPos.y then
            -- 第一、二象限
            ang = math.acos(deltalX / distance)
        else
            -- 第三、四象限
            ang = 2*math.pi - (math.acos(deltalX / distance));
        end

        -- 将弧度转成度
        ang = radiansToDegrees(ang);
        return ang;
    end

    -- 计算水平射线需要旋转的角度
    -- 由于setRotation是顺时针旋转的，因此需要取反
    local angle = -calcAngle();

    -- 射线光效的长度
    local unitLength = 126;

    local scaleX = distance / unitLength;

    trace("Effects", "angle = %d, distance = %d, scaleX = %f", angle, distance, scaleX);

    local node = cc.Node:create();
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    effectParent:addChild(node);
    node:setScaleX(scaleX);
    node:setPosition(cc.p(fromPos.x, fromPos.y + 45));  -- 修正一下射线光效的起始位置
    node:setRotation(angle);

    -- 播放射线光效
    playEffect(node, 3003);

    -- 射线攻击播放时长
    local duration = 0.3;

    local function callfunc()
        effectParent:removeChild(node);

        -- 3.射中目标效果
        playEffect(targetNode, 3005, 160, -50);

        finishEffect();

        -- 抛出魔藤攻击完毕事件
        EventMgr.fire(event.BINE_ATTACK_OVER);
    end;

    performWithDelay(sourceNode, callfunc, duration);
end

-- 播放魔藤施法动画
function playBineCast(sourceNode, targetNode)
    -- 1.魔藤吸血
    playEffect(targetNode, 3000, -105, 135);

    local function hpMove()
        -- 2.吸血飘血（从怪身上往魔藤身上飘）
        local fromPos = cc.p(targetNode:getPosition());
        local toPos   = cc.p(sourceNode:getPosition());

        local node = cc.Node:create();
        local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
        effectParent:addChild(node);
        node:setPosition(fromPos);
        playEffect(node, 3001, -120, -150);

        -- 触发出手事件，怪物掉血
        EventMgr.fire(event.PLAYER_MOVE, sourceNode.index);

        local callfunc = cc.CallFunc:create(function()
            effectParent:removeChild(node);

            -- 3.魔藤回血
            playEffect(sourceNode, 3002, -5, 30);

            local function backIdle()
                -- 抛出魔藤施法结束事件
                EventMgr.fire(event.BINE_CAST_OVER);
            end

            -- 回血时间
            local recoverTime = 0.5;
            performWithDelay(sourceNode, backIdle, recoverTime);

            finishEffect();
        end);

        local moveDuration = math.max(cc.pGetDistance(fromPos, toPos), 400) / speed;
        local moveTo = cc.MoveTo:create(moveDuration, toPos);
        node:runAction(cc.Sequence:create(moveTo, callfunc));
    end

    -- 吸血时间
    local absorbTime = 0.5;
    performWithDelay(targetNode, hpMove, absorbTime);
end

-- 播放收割灵魂动画
function playCollectSoul(sourceNode, targetNode, window, uifun, args)
    -- 1.收集灵魂
    playEffect(targetNode, 3000, -105, 135);

    local function hpMove()
        -- 2.灵魂移动（从怪身上往特殊技能飘）
        -- 目标位置先转成世界坐标
        local worldPos = sourceNode:convertToWorldSpaceAR(cc.p(0, 0));

        -- 再转到相对于sourceNode的父节点的坐标
        local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
        local toPos = effectParent:convertToNodeSpace(worldPos);
        local fromPos = cc.p(targetNode:getPosition());

        local node = cc.Node:create();
        local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
        effectParent:addChild(node);
        node:setPosition(fromPos);
        playEffect(node, 3001, -120, -150);

        local moveDuration = cc.pGetDistance(fromPos, toPos) / speed;
        local moveTo = cc.MoveTo:create(moveDuration, toPos);

        local callfunc = cc.CallFunc:create(function()
            effectParent:removeChild(node);

            finishEffect();
        end);

        node:runAction(cc.Sequence:create(moveTo, callfunc));
    end

    -- 收集灵魂时间
    local absorbTime = 0.5;
    performWithDelay(targetNode, hpMove, absorbTime);

    local function backIdle()
        -- 开始滚动数字
        uifun(window, args);
    end

    performWithDelay(sourceNode, backIdle, 1);
end

-- 吸能术效果
function playAbsorbEffect(targetNode)
    -- 1.吸血
    playEffect(targetNode, 3000, -105, 135);

    local function hpMove()
        -- 2.吸血飘血（从怪身上往人身上飘）
        local fromPos = cc.p(targetNode:getPosition());
        local toPos = UIDungeonMgr.getCurLevel():getHeroPos();

        local node = cc.Node:create();
        local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
        effectParent:addChild(node);
        node:setPosition(fromPos);
        playEffect(node, 3001, -120, -150);

        local callfunc = cc.CallFunc:create(function()
            effectParent:removeChild(node);

            -- 3.回血
            playEffect(effectParent, 3002, toPos.x - 5, toPos.y + 30);
        end);

        local moveDuration = math.max(cc.pGetDistance(fromPos, toPos), 400) / (speed * 2);
        local moveTo = cc.MoveTo:create(moveDuration, toPos);
        node:runAction(cc.Sequence:create(moveTo, callfunc));
    end

    -- 吸血时间
    local absorbTime = 0.5;
    performWithDelay(targetNode, hpMove, absorbTime);
end

-- 属性增加(美术字)飞升特效
function playAttribUpEffect(parentNode, addValue, offset, fieldName)
    if addValue <= 0 then
        return;
    end

    local fieldNameStr = FieldsM.getFieldName(fieldName);
    fieldNameStr = fieldNameStr .. " +" .. tostring(addValue);

    -- 不再用美术字，改用程序字
    playAttribUpEffect3(parentNode, offset, fieldNameStr);
end

-- 属性增加(普通字体)飞升特效
function playAttribUpEffect2(parentNode, offset, str)
    -- 飘字
    local fontSize = 50;
    local text = TextPoolM.createTextObject(fontSize);
    text:setString(str);
    text:setFontName(getLocFontName());
    text:setTextColor(TextStyleM.TEXT_COLOR_DARK_BROWN);
    text:enableOutline(TextStyleM.TEXT_OUTLINE_COLOR_BRIGHT, 2);
    text:setScale(0.8);
    text:setPosition(offset);
    parentNode:addChild(text);

    local scaleUp = cc.ScaleTo:create(0.2, 1.4);
    local scaleDown = cc.ScaleTo:create(0.15, 1.0);
    local move = cc.MoveBy:create(2, cc.p(0, 70));
    local delay = cc.DelayTime:create(1);
    local fadeOut = cc.FadeOut:create(1);
    local callFunc = cc.CallFunc:create(function()
        parentNode:removeChild(text);
    end);

    text:runAction(cc.Sequence:create(scaleUp, scaleDown, cc.Spawn:create(move, cc.Sequence:create(delay, fadeOut))));
end

-- 属性增加(金黄色字体)飞升特效
function playAttribUpEffect3(parentNode, offset, str)
    -- 飘字
    local text = ccui.Text:create();
    TextStyleM.setTextStyle(text, 34, TextStyleM.TEXT_COLOR_LIGHT_YELLOW, true);
    text:setString(str);
    text:setFontName(getLocFontName());

    -- 文本居中
    text:setTextHorizontalAlignment(cc.TEXT_ALIGNMENT_CENTER);
    text:enableShadow(TextStyleM.TEXT_OUTLINE_COLOR_DARK, cc.size(3, -3));

    text:setScale(0.8);
    text:setPosition(offset);
    parentNode:addChild(text);

    local scaleUp = cc.ScaleTo:create(0.15, 1.1);
    local scaleDown = cc.ScaleTo:create(0.05, 1.0);
    local move = cc.MoveBy:create(2, cc.p(0, 70));
    local delay = cc.DelayTime:create(1);
    local fadeOut = cc.FadeOut:create(1);
    local callFunc = cc.CallFunc:create(function()
        parentNode:removeChild(text);
    end);

    text:runAction(cc.Sequence:create(scaleUp, scaleDown, cc.Spawn:create(move, cc.Sequence:create(delay, fadeOut))));
end

-- 怪物出现特效
function playMonsterAppearEffect(grid, monsterNode, bgNode, barNode, effectId)
    monsterNode:setScale(0.8);
    monsterNode:setVisible(true);

    -- 播放怪物出现的效果
    if effectId then
        playEffect(monsterNode, effectId);
    else
        playEffect(monsterNode, 1172);
    end

    -- 先放大再缩小
    local function doEffect()
        if grid.gridData.type ~= GRID_TYPE_MONSTER and
            grid.gridData.type ~= GRID_TYPE_SUMMON and
            grid.gridData.type ~= GRID_TYPE_BOX then
            return;
        end

        if bgNode then
            bgNode:setVisible(true);
        end

        if barNode then
            barNode:setVisible(true);
        end

        local oldScale = monsterNode:getScale();
        local scaleUp = cc.ScaleTo:create(0.1, 1.1 * 1.25 * oldScale);
        local scaleDown = cc.ScaleTo:create(0.05, 1 * 1.25 * oldScale);
        monsterNode:runAction(cc.Sequence:create(scaleUp, scaleDown));
        monsterNode:setScale(1);
    end

    -- performWithDelay(monsterNode, doEffect, 0.1);
    doEffect();
end

-- 建筑出现特效
function playBuildingAppearEffect(grid, itemNode)
    itemNode:setScale(0.8);
    itemNode:setVisible(true);

    -- 播放怪物出现的效果
    playEffect(itemNode, 1172);

    -- 先放大再缩小
    local function doEffect()
        local oldScale = itemNode:getScale();
        local scaleUp = cc.ScaleTo:create(0.1, 1.1 * 1.25 * oldScale);
        local scaleDown = cc.ScaleTo:create(0.05, 1 * 1.25 * oldScale);
        itemNode:runAction(cc.Sequence:create(scaleUp, scaleDown));
        itemNode:setScale(1);
    end

    performWithDelay(itemNode, doEffect, 0.1);
end

-- 播放帐篷抖动
function playTentShake(sourceNode)
    sourceNode:setScale(0.8);
    sourceNode:setVisible(true);

    -- 播放怪物出现的效果
    playEffect(sourceNode, 1172);

    -- 先放大再缩小
    local function doEffect()
        local oldScale = sourceNode:getScale();
        local scaleUp = cc.ScaleTo:create(0.1, 1.1 * 1.25 * oldScale);
        local scaleDown = cc.ScaleTo:create(0.05, 1 * 1.25 * oldScale);
        sourceNode:runAction(cc.Sequence:create(scaleUp, scaleDown));
        sourceNode:setScale(1);
    end

    performWithDelay(sourceNode, doEffect, 0.1);
end

-- 进入地牢下一层时给卷轴的效果
function playSpellGainEffectOnEnter(targetNode, pos, itemId, num)
    if targetNode.gainItemList == nil then
        targetNode.gainItemList = {};
    end

    local iconPath = getItemIconPath(ItemM.query(itemId, "icon"));

    local name = ItemM.query(itemId, "name");
    for i = 1, num do
        table.insert(targetNode.gainItemList, name);
    end

    local function delay()
        -- 目标位置先转成世界坐标
        local worldPos = targetNode:convertToWorldSpaceAR(cc.p(0, 0));

        -- 再转到相对于sourceNode的父节点的坐标
        local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
        local toPos = effectParent:convertToNodeSpace(worldPos);
        local fromPos = cc.pAdd(pos, cc.p(0, 100) );

        local function callback()
            -- 卷轴图标闪一下
            playEffect(targetNode, 1106, 63, 53);

            local x = nil;
            local ap = cc.p(0.5, 0.5);

            if getLang() ~= "zh" and getLang() ~= "tw" then
                x = targetNode:getContentSize().width;
                ap = cc.p(1, 0.5);
            end

            showItemName(targetNode, TextStyleM.TEXT_COLOR_CYAN, x, nil, nil, ap);
        end

        commonItemsFlyTo(effectParent, fromPos, toPos, iconPath, num, callback);
    end
    performWithDelay(targetNode, delay, 1);
end

-- 进入地牢下一层时给宝物的效果
function playEquipGainEffectOnEnter(targetNode, pos, itemId, num)
    if targetNode.gainItemList == nil then
        targetNode.gainItemList = {};
    end

    local iconPath = getItemIconPath(ItemM.query(itemId, "icon"));

    local name = ItemM.query(itemId, "name");
    for i = 1, num do
        table.insert(targetNode.gainItemList, name);
    end

    local function delay()
        -- 目标位置先转成世界坐标
        local worldPos = targetNode:convertToWorldSpaceAR(cc.p(0, 0));

        -- 再转到相对于sourceNode的父节点的坐标
        local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
        local toPos = effectParent:convertToNodeSpace(worldPos);
        local fromPos = cc.pAdd(pos, cc.p(0, 100) );

        local function callback()
            -- 卷轴图标闪一下
            playEffect(targetNode, 1107, 60, 58);

            local x = nil;
            local ap = cc.p(0.5, 0.5);
            if getLang() ~= "zh" and getLang() ~= "tw" then
                x = 0;
                ap = cc.p(0, 0.5);
            end
            showItemName(targetNode, TextStyleM.TEXT_COLOR_CYAN, x, nil, nil, ap);
        end

        commonItemsFlyTo(effectParent, fromPos, toPos, iconPath, num, callback);
    end
    performWithDelay(targetNode, delay, 1);
end

-- 使用道具增加攻击的效果
function playAttackGainEffectOnEnter(targetNode, pos, textNode, iconNode, addValue)
    local curAttack = ME.user:getAttack();

    local oldValue = tonumber(textNode:getString());
    if curAttack == oldValue then
        textNode:setString(tostring(oldValue - addValue));
    end
    UIDungeonMgr:getCurLevel():setUserAttribShow("attack", textNode:getString());

    local function delay()
        local originToPosX = iconNode:getPositionX();
        local originToPosY = iconNode:getPositionY();

        -- 目标位置先转成世界坐标
        local worldPos = iconNode:convertToWorldSpaceAR(cc.p(0, 40));
        --print("originToPosX:"..originToPosY..",toPosY:"..originToPosY..", world posx:"..worldPos.x..",posy:"..worldPos.y);

        -- 再转到相对于effetNode的坐标
        local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
        local toPos = effectParent:convertToNodeSpace(worldPos);
        --print("toPosX:"..toPos.x..",toPosY:"..toPos.y);

        local fromPos = cc.pAdd(pos, cc.p(0, 100) );
        --print("fromPosX:"..fromPos.x..",fromPosY:"..fromPos.y);

        local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / speed;

        local angle = cc.pGetAngle(fromPos, toPos);
        angle = radiansToDegrees(angle);
        --print("angle:"..angle);

        local node = cc.Node:create();

        effectParent:addChild(node);
        local _, effect = playEffect(node, 1142);
        node:setPosition(fromPos);
        node:setRotation(angle+20);

        local callfunc = cc.CallFunc:create(function()
            effectParent:removeChild(node);

            local curAttack = ME.user:getAttack();
            textNode:setString(tostring(curAttack));
            UIDungeonMgr:getCurLevel():setUserAttribShow("attack", nil);
            -- 界面闪一下
            playEffect(iconNode, 1143, 30, 40);
            commonTextEffect(textNode, TextStyleM.TEXT_COLOR_LIGHT_YELLOW);
        end);

        local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), 2.0);
        node:runAction(cc.Sequence:create( moveTo, callfunc));
    end
    performWithDelay(targetNode, delay, 1);
end

-- 使用道具增加魔力的效果
function playMagicGainEffectOnEnter(targetNode, pos, textNode, iconNode, addValue)
    local curMagic = ME.user:getMagic();

    local oldValue = tonumber(textNode:getString());
    if curMagic == oldValue then
        textNode:setString(tostring(oldValue - addValue));
    end
    UIDungeonMgr:getCurLevel():setUserAttribShow("magic", textNode:getString());

    local function delay()
        local originToPosX = iconNode:getPositionX();
        local originToPosY = iconNode:getPositionY();

        -- 目标位置先转成世界坐标
        local worldPos = iconNode:convertToWorldSpaceAR(cc.p(0, 40));
        --print("originToPosX:"..originToPosY..",toPosY:"..originToPosY..", world posx:"..worldPos.x..",posy:"..worldPos.y);

        -- 再转到相对于effetNode的坐标
        local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
        local toPos = effectParent:convertToNodeSpace(worldPos);
        --print("toPosX:"..toPos.x..",toPosY:"..toPos.y);

        local fromPos = cc.pAdd(pos, cc.p(0, 100) );
        --print("fromPosX:"..fromPos.x..",fromPosY:"..fromPos.y);

        local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / speed;

        local angle = cc.pGetAngle(fromPos, toPos);
        angle = radiansToDegrees(angle);
        --print("angle:"..angle);

        local node = cc.Node:create();

        effectParent:addChild(node);
        local _, effect = playEffect(node, 1145);
        node:setPosition(fromPos);
        node:setRotation(angle+20);

        local callfunc = cc.CallFunc:create(function()
            effectParent:removeChild(node);

            local curMagic = ME.user:getMagic();
            textNode:setString(tostring(curMagic));
            UIDungeonMgr:getCurLevel():setUserAttribShow("magic", nil);

            -- 界面闪一下
            playEffect(iconNode, 1146, 30, 25);
            commonTextEffect(textNode, TextStyleM.TEXT_COLOR_LIGHT_YELLOW);
        end);

        local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), 2.0);
        node:runAction(cc.Sequence:create( moveTo, callfunc));
    end
    performWithDelay(targetNode, delay, 1);
end

-- 进入地牢下一层时生命值增加的效果
function playHpGainEffectOnEnter(targetNode, pos, textNode, iconNode, addValue)
    local curHp = ME.user:getHp();
    local delayHpEffect = ME.user.dbase:queryTemp("delayHpEffect");
    if delayHpEffect then
        -- 多加上需要扣除的
        curHp = curHp + delayHpEffect;
        ME.user.dbase:deleteTemp("delayHpEffect");
    end

    local oldValue = tonumber(textNode:getString());
    textNode:setString(tostring(curHp - addValue));
    UIDungeonMgr:getCurLevel():setUserAttribShow("hp", textNode:getString());

    local function delay()
        local originToPosX = iconNode:getPositionX();
        local originToPosY = iconNode:getPositionY();

        -- 目标位置先转成世界坐标
        local worldPos = iconNode:convertToWorldSpaceAR(cc.p(0, 40));
        --print("originToPosX:"..originToPosY..",toPosY:"..originToPosY..", world posx:"..worldPos.x..",posy:"..worldPos.y);

        -- 再转到相对于effetNode的坐标
        local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
        local toPos = effectParent:convertToNodeSpace(worldPos);
        --print("toPosX:"..toPos.x..",toPosY:"..toPos.y);

        local fromPos = cc.pAdd(pos, cc.p(0, 100) );
        --print("fromPosX:"..fromPos.x..",fromPosY:"..fromPos.y);

        local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / speed;

        local angle = cc.pGetAngle(fromPos, toPos);
        angle = radiansToDegrees(angle);
        --print("angle:"..angle);

        local node = cc.Node:create();

        effectParent:addChild(node);
        local _, effect = playEffect(node, 1116, 0, 0);
        node:setPosition(fromPos);
        node:setRotation(angle+20);

        local callfunc = cc.CallFunc:create(function()
            effectParent:removeChild(node);

            local curHp = ME.user:getHp();
            textNode:setString(tostring(curHp));
            UIDungeonMgr:getCurLevel():setUserAttribShow("hp", nil);

            if delayHpEffect then
                -- 多加上需要扣除的
                curHp = curHp + delayHpEffect;
                textNode:setString(tostring(curHp));
                local params = { ["damage"] = delayHpEffect, };
                local function callback()
                    UIDungeonMgr.getCurLevel():playHeroDamaged(params);
                    UIDungeonMgr.getCurLevel():updateUIByDesc("hp");
                end
                performWithDelay(targetNode, callback, 0.5);
            end

            -- 界面闪一下
            playEffect(iconNode, 1117, 25, 27);
            commonTextEffect(textNode, TextStyleM.TEXT_COLOR_BRIGHT_YELLOW);
        end);

        local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), 2.0);
        node:runAction(cc.Sequence:create( moveTo, callfunc));
    end
    performWithDelay(targetNode, delay, 1);
end

-- 进入地牢下一层时魔法值增加的效果
function playMpGainEffectOnEnter(targetNode, pos, textNode, iconNode, addValue)
    local curMp = ME.user:getMp();

    local showValue = textNode:getString();
    if textNode:getString() == "" then
        showValue = curMp - addValue;
    end

    local oldValue = tonumber(textNode:getString());
    if curMp == oldValue then
        textNode:setString(tostring(showValue));
    end

    UIDungeonMgr:getCurLevel():setUserAttribShow("mp", showValue);

    local function delay()
        local originToPosX = iconNode:getPositionX();
        local originToPosY = iconNode:getPositionY();

        -- 目标位置先转成世界坐标
        local worldPos = iconNode:convertToWorldSpaceAR(cc.p(0, 40));
        --print("originToPosX:"..originToPosY..",toPosY:"..originToPosY..", world posx:"..worldPos.x..",posy:"..worldPos.y);

        -- 再转到相对于effetNode的坐标
        local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
        local toPos = effectParent:convertToNodeSpace(worldPos);
        --print("toPosX:"..toPos.x..",toPosY:"..toPos.y);

        local fromPos = cc.pAdd(pos, cc.p(0, 100) );
        --print("fromPosX:"..fromPos.x..",fromPosY:"..fromPos.y);

        local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / speed;

        local angle = cc.pGetAngle(fromPos, toPos);
        angle = radiansToDegrees(angle);
        --print("angle:"..angle);

        local node = cc.Node:create();

        effectParent:addChild(node);
        local _, effect = playEffect(node, 1119, 0, 0);
        node:setPosition(fromPos);
        node:setRotation(angle+20);

        local callfunc = cc.CallFunc:create(function()
            effectParent:removeChild(node);

            local curMp = ME.user:getMp();
            textNode:setString(tostring(curMp));
            UIDungeonMgr:getCurLevel():setUserAttribShow("mp", nil);
            -- 界面闪一下
            playEffect(iconNode, 1120, 25, 27);
            commonTextEffect(textNode, TextStyleM.TEXT_COLOR_BRIGHT_YELLOW);
        end);

        local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), 2.0);
        node:runAction(cc.Sequence:create( moveTo, callfunc));
    end
    performWithDelay(targetNode, delay, 1);
end

-- 进入地牢下一层时探索点增加的效果
function playSpGainEffectOnEnter(targetNode, pos, textNode, iconNode, addValue)
    local curSp = ME.user:getSp();

    local oldValue = tonumber(textNode:getString());
    local showValue = oldValue;
    if oldValue == curSp then
        showValue = oldValue - addValue;
        textNode:setString(tostring(showValue));
    end

    UIDungeonMgr:getCurLevel():setUserAttribShow("search_point", showValue);

    local function delay()
        local iconPath = "images/ui/level/item/search_point1.png";

        -- 图标飞入到菜单右上角探索点处
        local originToPosX = iconNode:getPositionX();
        local originToPosY = iconNode:getPositionY();

        -- 目标位置先转成世界坐标
        local worldPos = iconNode:convertToWorldSpaceAR(cc.p(0, 0));
        --print("originToPosX:"..originToPosY..",toPosY:"..originToPosY..", world posx:"..worldPos.x..",posy:"..worldPos.y);

        -- 再转到相对于sourceNode的父节点的坐标
        local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
        local toPos = effectParent:convertToNodeSpace(worldPos);
        --print("toPosX:"..toPos.x..",toPosY:"..toPos.y);

        local fromPos = cc.pAdd(pos, cc.p(0, 100) );
        --print("fromPosX:"..fromPos.x..",fromPosY:"..fromPos.y);

        local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / (speed * 2);

        local icon = ccui.ImageView:create();
        icon:loadTexture(iconPath);
        icon:setPosition(fromPos);
        icon:setLocalZOrder(10000);

        local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();

        local callfunc = cc.CallFunc:create(function()
            effectParent:removeChild(icon);

            -- 探索点图标闪一下
            playEffect(iconNode, 1105, 26, 18);
            commonTextEffect2(textNode, TextStyleM.TEXT_COLOR_BRIGHT_YELLOW);
            local curSp = ME.user:getSp();
            textNode:setString(tostring(curSp));
            UIDungeonMgr:getCurLevel():setUserAttribShow("search_point", nil);
        end);

        local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), 2.0);
        local rotate = cc.RotateBy:create(duration, 180);
        local scaleTo = cc.ScaleTo:create(duration, 0.5);
        icon:runAction(cc.Sequence:create( cc.Spawn:create(moveTo,rotate,scaleTo), callfunc));
        effectParent:addChild(icon);
    end
    performWithDelay(targetNode, delay, 1);
end

function playIconZoomRepeat(node, scaleUp, scaleDown)
    local nodeScaleUp = cc.ScaleTo:create(0.5, scaleUp);
    local nodeScaleDown = cc.ScaleTo:create(0.5, scaleDown);
    node:runAction(cc.RepeatForever:create(cc.Sequence:create(nodeScaleUp, nodeScaleDown)));
end

-- 通用震动效果
function playShakeEffect(node, duration, offset, times, standardPos)
    if standardPos == nil then
        standardPos = cc.p(node:getPosition());
    end

    if node.shakeEffectInitPos == nil then
        node.shakeEffectInitPos = standardPos;
    end

    local pos = node.shakeEffectInitPos;
    local moveLeft = cc.MoveTo:create(duration, cc.p(pos.x-offset, pos.y));
    local moveDown = cc.MoveTo:create(duration, cc.p(pos.x, pos.y-offset));
    local moveRight = cc.MoveTo:create(duration, cc.p(pos.x+offset, pos.y));
    local moveUp = cc.MoveTo:create(duration, cc.p(pos.x, pos.y+offset));
    local callFunc = cc.CallFunc:create(function()
        -- 震动结束后回到初始坐标
        node:setPosition(pos);
    end);

    local action = cc.Sequence:create(moveLeft, moveDown, moveRight, moveUp, callFunc);
    node:runAction(cc.Repeat:create(action, times));
end

-- 通用震动效果(node位置会改变，开始节点)
function playShakeEffect2(node, duration, offset, times, standardPos)
    if standardPos == nil then
        standardPos = cc.p(node:getPosition());
    end

    local pos = standardPos;
    local moveLeft = cc.MoveTo:create(duration, cc.p(pos.x-offset, pos.y));
    local moveDown = cc.MoveTo:create(duration, cc.p(pos.x, pos.y-offset));
    local moveRight = cc.MoveTo:create(duration, cc.p(pos.x+offset, pos.y));
    local moveUp = cc.MoveTo:create(duration, cc.p(pos.x, pos.y+offset));
    local callFunc = cc.CallFunc:create(function()
        -- 震动结束后回到初始坐标
        node:setPosition(pos);
    end);

    local action = cc.Sequence:create(moveLeft, moveDown, moveRight, moveUp, callFunc);
    node:runAction(cc.Repeat:create(action, times));
end

-- 通用门震动效果
function playDoorShakeEffect(node, duration, offset, times, noRandom)
    if not noRandom then
        offset = offset + math.random(-1, 1);
    end

    local action;
    if math.random(1, 2) == 1 then
        local moveLeft = cc.MoveBy:create(duration, cc.p(-offset, 0));
        local moveDown = cc.MoveBy:create(duration, cc.p(0, -offset));
        local moveRight = cc.MoveBy:create(duration, cc.p(offset, 0));
        local moveUp = cc.MoveBy:create(duration, cc.p(0, offset));
        action = cc.Sequence:create(moveLeft, moveDown, moveRight, moveUp);
    else
        local moveLeft = cc.MoveBy:create(duration, cc.p(-offset, 0));
        local moveDown = cc.MoveBy:create(duration, cc.p(0, -offset));
        local moveRight = cc.MoveBy:create(duration, cc.p(offset, 0));
        local moveUp = cc.MoveBy:create(duration, cc.p(0, offset));

        action = cc.Sequence:create(moveUp, moveRight, moveDown, moveLeft);
    end

    node:runAction(cc.Repeat:create(action, times));
end

-- 播放帐篷怪物出现的黑影特效
function playMonsterAppearShadow(targetNode, scale)
    local node = cc.Node:create();
    node:setName("monster_shadow");

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    local child = findChildByName(effectParent, "monster_shadow");
    if child then
        effectParent:removeChild(child);
    end

    node:setPosition(targetNode:getPosition());
    playEffect(node, 1157, 0, 0, nil, scale);

    effectParent:addChild(node);
end

-- 播放资源怪物出现的烟尘特效
function playIntrudeMonsterAppearShadow(targetNode)
    local node = cc.Node:create();
    node:setName("intrude_shadow");

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    local child = findChildByName(effectParent, "intrude_shadow");
    if child then
        effectParent:removeChild(child);
    end

    node:setPosition(targetNode:getPosition());
    playEffect(node, 1254, 140, -80, nil, 0.6);

    effectParent:addChild(node);
end

-- 播放建筑消失特效(该接口适用于可淡出消失的建筑)
function playBuildingDisappear(buidlingNode)
    if buidlingNode.icon == nil then
        return;
    end

    -- 整个节点逐渐缩小效果
    local imgNode = buidlingNode.item;
    local scaleUp = cc.ScaleTo:create(0.1, 1);
    local scaleDown = cc.ScaleTo:create(0.75, 0.8);

    -- 打开透明度设置传递开关
    local iconImg = buidlingNode.icon;
    if iconImg then
        iconImg:setCascadeOpacityEnabled(true);
    end
    imgNode:setCascadeOpacityEnabled(true);

    -- 图片渐出
    local fadeOut = cc.FadeOut:create(1);

    local callFunc = cc.CallFunc:create(function()
        imgNode:removeAllChildren();
        buidlingNode.icon = nil;
    end);

    imgNode:runAction(cc.Sequence:create(scaleUp, scaleDown, fadeOut, callFunc));
    imgNode.isRemoving = true;
end

-- 播放建筑消失特效（该接口适用于无法淡出消失的建筑）
function playBuildingDisappear2(buidlingNode)
    -- 整个节点逐渐缩小效果
    local imgNode = buidlingNode.item;

    local speedCoef = 1.5;

    local scaleUp = cc.ScaleTo:create(0.1/speedCoef, 1);
    local scaleDown = cc.ScaleTo:create(0.75/speedCoef, 0.85);

    local callFunc = cc.CallFunc:create(function()
        imgNode:removeAllChildren();
        buidlingNode.icon = nil;

        -- 播放烟雾特效
        buidlingNode:playEffect(buidlingNode, "animation/open_block.csb", 0.5, nil, 1);
    end);

    imgNode:runAction(cc.Sequence:create(scaleUp, scaleDown, callFunc));
    imgNode.isRemoving = true;
end

-- 节点消失特效
function playNodeDisappear(parentNode, node)
    local speedCoef = 1.5;

    local scaleUp = cc.ScaleTo:create(0.1/speedCoef, 1);
    local scaleDown = cc.ScaleTo:create(0.75/speedCoef, 0.85);

    local callFunc = cc.CallFunc:create(function()
        setOpacity(node, 0);

        parentNode:playEffect(parentNode, "animation/open_block.csb", 0.5, nil, 1);
    end);

    node:runAction(cc.Sequence:create(scaleUp, scaleDown, callFunc));
end

-- 播放元素圣坛施法特效
function playElemMagicEffect(color, direction, sourceNode, targetNode, backFun, strengthen)
    local fromPos = cc.p(sourceNode:getPosition());

    -- 所有充能特效
    local chargeEffects = { ["blue"] = 2036, ["green"] = 2037, ["red"] = 2038, ["yellow"] = 2039 };

    -- 所有射线特效
    local lineEffects = { ["blue"] = 2040, ["green"] = 2041, ["red"] = 2042, ["yellow"] = 2043 };

    -- 所有爆炸特效
    local boomEffects = { ["blue"] = 2044, ["green"] = 2045, ["red"] = 2046, ["yellow"] = 2047 };

    -- 待播放的充能特效
    local chargeEffectId = chargeEffects[color];

    local node = cc.Node:create();
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    effectParent:addChild(node);
    local chargeEffNode = cc.Node:create();
    effectParent:addChild(chargeEffNode);
    local boomEffNode = cc.Node:create();
    effectParent:addChild(boomEffNode);

    -- 根据方向修正射线的起始位置
    if direction == "left" then
        local elemIconNode = findChildByName(sourceNode.icon, "left_node");
        fromPos = cc.pAdd(fromPos, cc.p(elemIconNode:getPosition()));
        fromPos = cc.pAdd(fromPos, cc.p(-70, -23));

        chargeEffNode:setPosition(cc.p(fromPos.x, fromPos.y));
        -- 1.播放充能特效
        playEffect(chargeEffNode, chargeEffectId, 0, 0);

    elseif direction == "right" then
        local elemIconNode = findChildByName(sourceNode.icon, "right_node");
        fromPos = cc.pAdd(fromPos, cc.p(elemIconNode:getPosition()));
        fromPos = cc.pAdd(fromPos, cc.p(-55, -28));

        chargeEffNode:setPosition(cc.p(fromPos.x, fromPos.y));
        -- 1.播放充能特效
        playEffect(chargeEffNode, chargeEffectId, 0, 0);

    elseif direction == "mid" then
        local elemIconNode = findChildByName(sourceNode.icon, "mid_node");
        fromPos = cc.pAdd(fromPos, cc.p(elemIconNode:getPosition()));
        fromPos = cc.pAdd(fromPos, cc.p(-59, -33));

        chargeEffNode:setPosition(cc.p(fromPos.x, fromPos.y));
        -- 1.播放充能特效
        playEffect(chargeEffNode, chargeEffectId, 0, 0);
    else
        trace("Effects", "未知的方向：%s", tostring(direction));
    end

    if not targetNode then
        effectParent:removeChild(node);
        effectParent:removeChild(chargeEffNode);
        effectParent:removeChild(boomEffNode);
        -- 没有目标，就到这儿吧
        if backFun then
            backFun();
        end

        return;
    end

    -- 目标位置
    local toPos = cc.p(targetNode:getPosition());
    toPos = cc.pAdd(toPos, cc.p(-6, 33));

    trace("Effect", "[playElemMagicEffect]fromPosX = %d, fromPosY = %d", fromPos.x, fromPos.y);
    trace("Effect", "[playElemMagicEffect]toPosX = %d, toPosY = %d", toPos.x, toPos.y);

    -- 2.创建射线轨迹
    -- 计算两点间的距离
    local distance = cc.pGetDistance(fromPos, toPos);

    -- 计算水平射线需要旋转的角度
    -- 由于setRotation是顺时针旋转的，因此需要取反
    local angle = -calcPointAngle(fromPos, toPos);

    -- 射线光效的长度
    local unitLength = 126;

    local scaleX = distance / unitLength;

    trace("Effects", "angle = %d, distance = %d, scaleX = %f", angle, distance, scaleX);

    -- local node = cc.Node:create();
    -- local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    -- effectParent:addChild(node);
    node:setScaleX(scaleX);

    if strengthen == true then
        node:setScaleY(2);
    end

    node:setPosition(cc.p(fromPos.x, fromPos.y));  -- 修正一下射线光效的起始位置
    node:setRotation(angle);
    boomEffNode:setPosition(cc.p(toPos.x, toPos.y));

    -- 播放射线光效
    local lineEffectId = lineEffects[color];
    playEffect(node, lineEffectId);

    -- 射线攻击播放时长
    local duration = 0.6;

    --local function callfunc()
        -- 3.射中目标效果
    local boomEffectId = boomEffects[color];
    playEffect(boomEffNode, boomEffectId, 0, 0);
    --end;

    local function finishLineEff()
        effectParent:removeChild(node);
        effectParent:removeChild(chargeEffNode);
        effectParent:removeChild(boomEffNode);

        finishEffect();

         -- 触发出手事件
        EventMgr.fire(event.PLAYER_MOVE, sourceNode.gridData:getPos());

        if backFun ~= nil then
            backFun();
        end
    end

    -- performWithDelay(sourceNode, callfunc, 0.1);
    performWithDelay(sourceNode, finishLineEff, duration);

    return duration;
end

-- 播放元素圣坛爆裂的光效
function playElemBoomEffect(sourceNode, args)
    local fromPos = cc.p(sourceNode:getPosition());

    local node = cc.Node:create();
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    effectParent:addChild(node);
    node:setPosition(cc.pAdd(fromPos, cc.p(0, 32)));

    playEffect(node, 2048);

    if args.gridType == GRID_TYPE_SKY_ELE_ALTAR then
        disappear = false;
    else
        disappear = true;
    end

    local function callfunc()
        effectParent:removeChild(node);

        if args.bonus then
            -- 如果获得了奖励，播放奖励动画
            sourceNode:onElemAltarBonus(args.bonus, disappear);
        elseif args.summon then
            -- 如果触发了召唤事件，刷新一下格子
            -- 先把圣坛图标移除
            if disappear then
                sourceNode.item:removeAllChildren();
                sourceNode.icon = nil;
            end

            UIDungeonMgr:getCurLevel():doCacheTransform();
        elseif args.magic then
            -- 施放了魔法
            -- 英雄施法表现
            local skillId = args.magic;
            UIDungeonMgr.getCurLevel():whenApplyMagic(skillId);

            -- 播放建筑消失特效
            if disappear then
                playBuildingDisappear(sourceNode);
            end
        elseif disappear then
            -- 播放建筑消失特效
            playBuildingDisappear(sourceNode);
        end

        -- 抛出事件
        EventMgr.fire(event.PLAYER_MOVE, sourceNode.gridData:getPos());
    end

    local duration = 2.3;
    performWithDelay(sourceNode, callfunc, duration);

    local function playScreenShining()
        -- 显示全屏星光特效
        showScreenShining();
    end

    performWithDelay(sourceNode, playScreenShining, 1.8);
    return duration;
end

-- 播放元素圣坛爆裂的光效
function playLordBoomEffect(sourceNode, args)
    sourceNode.icon:setVisible(false);
    local fromPos = cc.p(sourceNode:getPosition());

    local node = cc.Node:create();
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    effectParent:addChild(node);
    node:setPosition(cc.pAdd(fromPos, cc.p(0, 32)));

    playEffect(node, 1321, 0, -25);

    local function callfunc()
        effectParent:removeChild(node);

        sourceNode.item:removeAllChildren();
        sourceNode.icon = nil;
        UIDungeonMgr:getCurLevel():doCacheTransform();

        -- 抛出事件
        EventMgr.fire(event.PLAYER_MOVE, sourceNode.gridData:getPos());
    end

    local duration = 2;
    performWithDelay(sourceNode, callfunc, duration);

    local function playScreenShining()
        -- 显示全屏星光特效
        showScreenShining();
    end

    performWithDelay(sourceNode, playScreenShining, 1.5);
    return duration;
end

-- 直线投掷特效
-- 创建道具节点的图片需要是垂直向上的
function playStraightLineEffect(iconPath, fromPos, targetNode, speedFix, effectId, effectOffset, propDbase, audioName, moreTargetList, bonus, tailId, rotateOffset, iconLength)
    local effectParent = UIDungeonMgr.getCurLevel():getEffectParent();

    -- 飞行
    local bulletNode = cc.Node:create();
    local bulletImg = ccui.ImageView:create(iconPath);
    bulletImg:setAnchorPoint(0.5, 1);
    bulletImg:setPosition(cc.p(2, 30));
    bulletNode:addChild(bulletImg);
    bulletNode:setPosition(fromPos);
    effectParent:addChild(bulletNode);

    -- 创建光效拖尾
    if tailId then
        -- 创建光效节点
        local _, node = playEffect(bulletNode, tailId);

        -- 旋转光效，让它和父物件的旋转方向保持一致
        local rotate = math.abs(rotateOffset) - 3;
        node:setRotation(rotate);

        -- 移动光效到父物件的尾部
        local offset = -iconLength;
        node:setPosition(offset * math.cos(math.rad(rotate)) + 10, offset * math.sin(math.rad(rotate)));
    end

    -- 飞行时间
    local toPos = cc.p(targetNode:getPosition());
    local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / (2 * speed * speedFix);

    -- 计算飞镖旋转角度
    -- 通过calcPointAngle计算出来的是与水平方向的夹角，而图片默认是垂直方向顺时针旋转45度的，因此还要减去45度
    -- 由于setRotation是顺时针旋转的，因此需要取反
    local angle = -(calcPointAngle(fromPos, toPos) - rotateOffset);
    bulletNode:setRotation(angle);

    -- 初始化打击光效
    local callback = cc.CallFunc:create(function()
        local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
        effectParent:removeChild(bulletNode);

        if targetNode.mirror then
            -- 魔镜，不要打击光效
            playMirrorEffect(targetNode);
            EventMgr.fire(event.PLAYER_MOVE, targetNode.gridData:getPos());
            return;
        end

        -- 设置光效偏移
        local offsetX = effectOffset.x;
        local offsetY = effectOffset.y;

        -- 目标受创
        playEffect(targetNode, effectId, offsetX, offsetY);

        -- 还有别的目标
        if moreTargetList and type(moreTargetList) == "table" then
            local bossEffect   = propDbase["boss_effect"];
            local bossEffectId = bossEffect and bossEffect["effect_id"] or 3006;
            local bossEffectOffset = bossEffect and cc.p(bossEffect["offset"][1], bossEffect["offset"][2]) or cc.p(0, -50);
            local bossOffsetX = bossEffectOffset.x;
            local bossOffsetY = bossEffectOffset.y;

            for _, elseTarget in pairs(moreTargetList) do
                if elseTarget[1] == "boss" then
                    playEffect(elseTarget[2], bossEffectId, bossOffsetX, bossOffsetY);
                else
                    playEffect(elseTarget[2], effectId, offsetX, offsetY);
                end
            end
        end

        -- 抛出事件
        EventMgr.fire(event.PLAYER_MOVE, targetNode.gridData:getPos());

        -- 如果有奖励，播放飞入包裹的动画
        if type(bonus) == "table" then
            targetNode:onPickUp(bonus, nil, true, nil, nil, true);
        end

    end);

    local moveTo = cc.MoveTo:create(duration, toPos);
    bulletNode:runAction(cc.Sequence:create(moveTo, callback));

    return duration, node;
end

-- 旋转投掷特效
function playRotateEffect(iconPath, fromPos, targetNode, speedFix, effectId, effectOffset, propDbase, audioName, moreTargetList, bonus)
    -- 初始化投掷环境变量
    local toPos = cc.p(targetNode:getPosition());
    local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / (1.5*speed*speedFix);

    -- 创建道具图片
    local itemImg = ccui.ImageView:create(iconPath);
    local node = cc.Node:create();
    node:setPosition(fromPos);
    node:addChild(itemImg);

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    effectParent:addChild(node);

    -- 初始化打击光效
    local callback = cc.CallFunc:create(function()
        local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
        effectParent:removeChild(node);

        if targetNode.mirror then
            -- 魔镜，不要打击光效
            playMirrorEffect(targetNode);
            EventMgr.fire(event.PLAYER_MOVE, targetNode.gridData:getPos());
            return;
        end

        -- 设置光效偏移
        local offsetX = effectOffset.x;
        local offsetY = effectOffset.y;

        -- 目标受创
        playEffect(targetNode, effectId, offsetX, offsetY);

        -- 还有别的目标
        if moreTargetList and type(moreTargetList) == "table" then
            local bossEffect   = propDbase["boss_effect"];
            local bossEffectId = bossEffect and bossEffect["effect_id"] or 3006;
            local bossEffectOffset = bossEffect and cc.p(bossEffect["offset"][1], bossEffect["offset"][2]) or cc.p(0, -50);
            local bossOffsetX = bossEffectOffset.x;
            local bossOffsetY = bossEffectOffset.y;

            for _, elseTarget in pairs(moreTargetList) do
                if elseTarget[1] == "boss" then
                    playEffect(elseTarget[2], bossEffectId, bossOffsetX, bossOffsetY);
                else
                    playEffect(elseTarget[2], effectId, offsetX, offsetY);
                end
            end
        end

        -- 抛出事件
        EventMgr.fire(event.PLAYER_MOVE, targetNode.gridData:getPos());

        -- 如果有奖励，播放飞入包裹的动画
        if type(bonus) == "table" then
            targetNode:onPickUp(bonus, nil, true, nil, nil, true);
        end

    end);

    local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), duration);
    local rotate = cc.RotateBy:create(duration, 720);
    node:runAction(cc.Sequence:create(cc.Spawn:create(moveTo, rotate), callback));

    return duration, node;
end

-- 投掷道具特效
function playPropertyHitAnimation(targetNode, iconPath, fromPos, moveType, effectId, effectOffset, propDbase, audioName, extraPara, bonus, tailId, rotateOffset, isSync, iconLength)
    -- 播放音效
    AudioM.playFx(audioName);

    -- 初始化投掷环境变量
    local moreTargetList;
    local isTreeHouse;
    if extraPara then
        moreTargetList = extraPara.moreTargetList;
        isTreeHouse = extraPara.treeHouse;
    end

    -- 速度增量
    local speedFix = 1;
    if isTreeHouse then
        speedFix = 2;
    end

    local duration = 0;

    -- 播放投掷轨迹(目前只支持直线和旋转两种)
    local function hitAnimation(node)
        local duration = 0;
        if moveType and moveType == LaunchType.STRAIGHT then
            duration = playStraightLineEffect(iconPath, fromPos, node, speedFix, effectId, effectOffset, propDbase, audioName, moreTargetList, bonus, tailId, rotateOffset, iconLength);
        else
            duration = playRotateEffect(iconPath, fromPos, node, speedFix, effectId, effectOffset, propDbase, audioName, moreTargetList, bonus);
        end

        return duration;
    end

    -- 判断是否打击多个目标
    if type(targetNode) == "table" then
        -- 是否同时打击
        if isSync then
            local tempDuration = 0;
            for _, c in pairs(targetNode) do
                tempDuration = hitAnimation(c);
                if tempDuration > duration then
                    duration = tempDuration;
                end
            end
        else
            -- 时间偏移指针
            local timePoint = 0;

            for _, node in pairs(targetNode) do
                local hit = hitAnimation;
                performWithDelay(node, hit, timePoint);

                timePoint = timePoint + 0.23;
            end
        end
    else
        duration = hitAnimation(targetNode);
    end

    return duration;
end

-- 播放投掷道具攻击目标的特效
function playThowPropertyAttack(iconPath, fromPos, targetNode, isBOSS, extraPara, bonus)
    local moreTargetList;
    local isTreeHouse;
    if extraPara then
        moreTargetList = extraPara.moreTargetList;
        isTreeHouse = extraPara.treeHouse;
    end
    local speedFix = 1;
    if isTreeHouse then
        speedFix = 2;
    end

    local toPos = cc.p(targetNode:getPosition());
    local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / (1.5*speed*speedFix);

    -- 创建道具图片
    local itemImg = ccui.ImageView:create(iconPath);
    local node = cc.Node:create();
    node:setPosition(fromPos);
    node:addChild(itemImg);

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    effectParent:addChild(node);

    local callfunc = cc.CallFunc:create(function()
        effectParent:removeChild(node);

        -- 目标受创
        if isBOSS then
            playEffect(targetNode, 3006, 0, -50);
        else
            playEffect(targetNode, 3006, 0, 10);
        end

        if moreTargetList and type(moreTargetList) == "table" then
            -- 还有别的目标
            for _, elseTarget in pairs(moreTargetList) do
                if elseTarget[1] == "boss" then
                    playEffect(elseTarget[2], 3006, 0, -50);
                else
                    playEffect(elseTarget[2], 3006, 0, 10);
                end
            end
        end

        EventMgr.fire(event.PLAYER_MOVE, targetNode.gridData:getPos());

        -- 如果有奖励，播放飞入包裹的动画
        if type(bonus) == "table" then
            targetNode:onPickUp(bonus, nil, true, nil, nil, true);
        end
    end);

    local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), duration);
    local rotate = cc.RotateBy:create(duration, 720);
    node:runAction(cc.Sequence:create(cc.Spawn:create(moveTo, rotate), callfunc));
    return duration;
end

-- 播放使用火把攻击目标的特效
function playTorchAttack(iconPath, fromPos, targetNode, isBOSS, extraPara)
    local moreTargetList;
    local isTreeHouse;
    if extraPara then
        moreTargetList = extraPara.moreTargetList;
        isTreeHouse = extraPara.treeHouse;
    end
    local speedFix = 1;
    if isTreeHouse then
        speedFix = 2;
    end

    local toPos = cc.p(targetNode:getPosition());
    local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / (1.5*speed*speedFix);

    -- 创建道具图片
    local itemImg = ccui.ImageView:create(iconPath);
    local node = cc.Node:create();
    node:setPosition(fromPos);
    node:addChild(itemImg);

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    effectParent:addChild(node);

    local callfunc = cc.CallFunc:create(function()
        effectParent:removeChild(node);

        -- 目标受创
        if isBOSS then
            playEffect(targetNode, 1229, 0, -50);
        else
            playEffect(targetNode, 1139, 0, 10);
        end

        if moreTargetList and type(moreTargetList) == "table" then
            -- 还有别的目标
            for _, elseTarget in pairs(moreTargetList) do
                if elseTarget[1] == "boss" then
                    playEffect(elseTarget[2], 1229, 0, -50);
                else
                    playEffect(elseTarget[2], 1139, 0, 10);
                end
            end
        end

        EventMgr.fire(event.PLAYER_MOVE, targetNode.gridData:getPos());
    end);

    local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), duration);
    local rotate = cc.RotateBy:create(duration, 720);
    node:runAction(cc.Sequence:create(cc.Spawn:create(moveTo, rotate), callfunc));
    return duration;
end

-- 播放使用炸弹攻击目标的特效
function playBombAttack(iconPath, fromPos, targetNode, isBOSS, extraPara)
    local moreTargetList;
    local isTreeHouse;
    if extraPara then
        moreTargetList = extraPara.moreTargetList;
        isTreeHouse = extraPara.treeHouse;
    end
    local speedFix = 1;
    if isTreeHouse then
        speedFix = 2;
    end

    local toPos = cc.p(targetNode:getPosition());
    local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / (1.5*speed*speedFix);

    -- 创建道具图片
    local itemImg = ccui.ImageView:create(iconPath);
    local node = cc.Node:create();
    node:setPosition(fromPos);
    node:addChild(itemImg);

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    effectParent:addChild(node);

    local callfunc = cc.CallFunc:create(function()
        effectParent:removeChild(node);

        -- 目标受创
        if isBOSS then
            playEffect(targetNode, 1234, 0, 20);
        else
            playEffect(targetNode, 1233, 0, 20);
        end

        if moreTargetList and type(moreTargetList) == "table" then
            -- 还有别的目标
            for _, elseTarget in pairs(moreTargetList) do
                if elseTarget[1] == "boss" then
                    playEffect(elseTarget[2], 1229, 0, -50);
                else
                    playEffect(elseTarget[2], 1139, 0, 10);
                end
            end
        end

        EventMgr.fire(event.PLAYER_MOVE, targetNode.gridData:getPos());
    end);

    local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), duration);
    local rotate = cc.RotateBy:create(duration, 720);
    node:runAction(cc.Sequence:create(cc.Spawn:create(moveTo, rotate), callfunc));
    return duration;
end

-- 播放沙漠之鹰攻击动画
function playEagleAttack(fromPos, targetNode, extraPara)
    local isTreeHouse;
    if extraPara then
        isTreeHouse = extraPara.treeHouse;
    end
    local speedFix = 1;
    if isTreeHouse then
        speedFix = 2;
    end

    -- 1.子弹壳爆裂
    local shellNode = cc.Node:create();
    local effectParent = UIDungeonMgr.getCurLevel():getEffectParent();
    playEffect(shellNode, 1213);
    shellNode:setPosition(cc.pAdd(fromPos, cc.p(0, 150)));
    effectParent:addChild(shellNode);

    -- 2.子弹飞行
    local bulletNode = cc.Node:create();
    local tailImg = ccui.ImageView:create(getLevelItemIconPath("eagle_bullet_tail"));
    local bulletImg = ccui.ImageView:create(getLevelItemIconPath("eagle_bullet"));
    bulletImg:setAnchorPoint(0.5, 1);
    tailImg:setAnchorPoint(0.5, 1);
    bulletImg:setPosition(cc.p(2, 30));
    tailImg:setPosition(cc.p(4, 0));
    bulletNode:addChild(tailImg);
    bulletNode:addChild(bulletImg);
    bulletNode:setPosition(fromPos);
    effectParent:addChild(bulletNode);

    -- 子弹飞行时间
    local toPos = cc.p(targetNode:getPosition());
    local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / (2.5*speed*speedFix);

    -- 计算子弹旋转角度
    -- 通过calcPointAngle计算出来的是与水平方向的夹角，而子弹图片默认是垂直向上的，因此还要减去90度
    -- 由于setRotation是顺时针旋转的，因此需要取反
    local angle = -(calcPointAngle(fromPos, toPos) - 90);
    bulletNode:setRotation(angle);

    local callfunc = cc.CallFunc:create(function()
        -- 移除子弹
        effectParent:removeChild(bulletNode);

        if targetNode.mirror then
            -- 魔镜，不要打击光效
            playMirrorEffect(targetNode);
            EventMgr.fire(event.PLAYER_MOVE, targetNode.gridData:getPos());
            return;
        end

        -- 3.击中目标特效
        local hitNode1 = cc.Node:create();
        playEffect(hitNode1, 1214);
        hitNode1:setPosition(toPos);
        effectParent:addChild(hitNode1);

        local hitNode2 = cc.Node:create();
        hitNode2:setPosition(toPos);
        effectParent:addChild(hitNode2);

        -- 4.溅血
        local function playBloodEffect()
            playEffect(hitNode2, 1215);

            local function remove()
                -- 移除特效
                effectParent:removeChild(shellNode);
                effectParent:removeChild(hitNode1);
                effectParent:removeChild(hitNode2);
            end

            performWithDelay(targetNode, remove, 0.8);
        end

        local function fire()
            -- 抛出回合事件
            EventMgr.fire(event.PLAYER_MOVE, targetNode.gridData:getPos());
        end
        performWithDelay(targetNode, fire, 0.1);

        -- 击中目标后稍微停顿一下再开始播放溅血特效
        AudioM.playFx("pistol_hit");
        performWithDelay(targetNode, playBloodEffect, 0.2);
    end);

    local moveTo = cc.MoveTo:create(duration, toPos);
    bulletNode:runAction(cc.Sequence:create(moveTo, callfunc));
    return duration;
end

-- 播放飞镖攻击动画
function playDartAttack(fromPos, targetNode, extraPara)
    local isTreeHouse;
    if extraPara then
        isTreeHouse = extraPara.treeHouse;
    end
    local speedFix = 1;
    if isTreeHouse then
        speedFix = 2;
    end

    local effectParent = UIDungeonMgr.getCurLevel():getEffectParent();

    -- 飞行
    local bulletNode = cc.Node:create();
    local bulletImg = ccui.ImageView:create(getLevelItemIconPath("dart"));
    bulletImg:setAnchorPoint(0.5, 1);
    bulletImg:setPosition(cc.p(2, 30));
    bulletNode:addChild(bulletImg);
    bulletNode:setPosition(fromPos);
    effectParent:addChild(bulletNode);

    -- 飞行时间
    local toPos = cc.p(targetNode:getPosition());
    local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / (2 * speed * speedFix);

    -- 计算飞镖旋转角度
    -- 通过calcPointAngle计算出来的是与水平方向的夹角，而飞镖图片默认是垂直向上的，因此还要减去90度
    -- 由于setRotation是顺时针旋转的，因此需要取反
    local angle = -(calcPointAngle(fromPos, toPos) - 90);
    bulletNode:setRotation(angle);

    local callfunc = cc.CallFunc:create(function()
        -- 移除子弹
        effectParent:removeChild(bulletNode);

        -- 3.击中目标特效
        local hitNode1 = cc.Node:create();
        playEffect(hitNode1, 1214);
        hitNode1:setPosition(toPos);
        effectParent:addChild(hitNode1);

        local hitNode2 = cc.Node:create();
        hitNode2:setPosition(toPos);
        effectParent:addChild(hitNode2);

        -- 4.溅血
        local function playBloodEffect()
            playEffect(hitNode2, 1215);
            local function remove()
                -- 移除特效
                effectParent:removeChild(hitNode1);
                effectParent:removeChild(hitNode2);
            end

            performWithDelay(targetNode, remove, 0.8);
        end

        local function fire()
            -- 抛出回合事件
            EventMgr.fire(event.PLAYER_MOVE, targetNode.gridData:getPos());
        end
        performWithDelay(targetNode, fire, duration + 0.2);

        -- 击中目标后稍微停顿一下再开始播放溅血特效
        -- AudioM.playFx("pistol_hit");
        performWithDelay(targetNode, playBloodEffect, 0.1);
    end);

    local moveTo = cc.MoveTo:create(duration, toPos);
    bulletNode:runAction(cc.Sequence:create(moveTo, callfunc));
    return duration;
end

-- 播放消除特性特效
function playKryptoniteAttack(iconPath, fromPos, targetNode, isBOSS, extraPara)
    local moreTargetList;
    local isTreeHouse;
    if extraPara then
        moreTargetList = extraPara.moreTargetList;
        isTreeHouse = extraPara.treeHouse;
    end
    local speedFix = 1;
    if isTreeHouse then
        speedFix = 2;
    end

    local toPos = cc.p(targetNode:getPosition());
    local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / (1.5*speed*speedFix);

    -- 创建道具图片
    local itemImg = ccui.ImageView:create(iconPath);
    local node = cc.Node:create();
    node:setPosition(fromPos);
    node:addChild(itemImg);

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    effectParent:addChild(node);

    local callfunc = cc.CallFunc:create(function()
        effectParent:removeChild(node);

        -- 目标受创
        if isBOSS then
            playEffect(targetNode, 54, 0, -50);
        else
            playEffect(targetNode, 54, 0, 10);
        end

        if moreTargetList and type(moreTargetList) == "table" then
            -- 还有别的目标
            for _, elseTarget in pairs(moreTargetList) do
                if elseTarget[1] == "boss" then
                    playEffect(elseTarget[2], 54, 0, -50);
                else
                    playEffect(elseTarget[2], 54, 0, 10);
                end
            end
        end

        EventMgr.fire(event.PLAYER_MOVE, targetNode.gridData:getPos());
    end);

    local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), duration);
    local rotate = cc.RotateBy:create(duration, 720);
    node:runAction(cc.Sequence:create(cc.Spawn:create(moveTo, rotate), callfunc));
    return duration;
end

-- 播放点击效果
function playClickEffect(targetNode, offsetX, offsetY)
    local x = offsetX or 0;
    local y = offsetY or 0;
    playEffect(targetNode, 1176, 0 + x, 0 + y);
end

-- 播放翻转的点击效果
function playClickEffectInvert(targetNode, offsetX, offsetY)
    local x = offsetX or 0;
    local y = offsetY or 0;
    playEffect(targetNode, 1326, 0 + x, 0 + y);
end

-- 播放拖动效果
function playDragEffect(targetNode)
    playEffect(targetNode, 1177, 0, 0);
end

-- 创建特效动画
function createEffectAnim(effectId)
    local effectInfo = EffectM.query(effectId);
    local effectPath = effectInfo["path"];
    local effectSrc  = effectInfo["effect_src"];
    local offset     = effectInfo["off_set"];
    local scale      = effectInfo["scale"];
    local speed      = effectInfo["speed"];

    local filePath = effectPath .. "/" .. effectSrc .. ".csb";
    local effectAniam = EffectAnimation.create(filePath, speed);

    effectAniam:setScale(scale);
    effectAniam:setPosition(offset[1], offset[2]);

    return effectAniam;
end

-- 播放排炮攻击动画
function playCannonAttackEffect(sourceNode, targetNodes)
    -- 1.启动
    sourceNode.animation:play(0, 23);

    -- 2.发射炮弹
    -- 启动时间
    local prepareTime = 0.3;

    -- 发射
    local function launch()
        -- 炮弹起始位置（暂时仅支持炮口朝上）
        local fromPos = cc.pAdd(cc.p(sourceNode:getPosition()), cc.p(0, 100));

        -- 获取炮弹的图标路径
        local bulletPath = getLevelItemIconPath("bullet2");

        -- 绘制炮弹
        local bulletImg = ccui.ImageView:create(bulletPath);
        local node = cc.Node:create();
        local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
        effectParent:addChild(node);
        node:setPosition(fromPos);
        node:addChild(bulletImg);

        bulletImg:setOpacity(255);

        -- 速度系数
        local speedCoef = 3.0;

        if #targetNodes > 0 then
            -- 计算到达各个节点所需的时间
            local delayList = {};
            for _, targetNode in pairs(targetNodes) do
                local toPos = cc.p(targetNode:getPosition());
                local duration = cc.pGetDistance(fromPos, toPos)/(speedCoef*speed);
                table.insert(delayList, duration);

                -- cclog("格子：%d, delay = %f", targetNode.gridData:getPos(), duration);
            end

            -- 获取该方向上最远的目标
            local farmostNode = targetNodes[#targetNodes];
            local farmostPos = cc.p(farmostNode:getPosition());
            local duration = cc.pGetDistance(fromPos, farmostPos)/(speedCoef*speed);

            local callfunc = cc.CallFunc:create(function()
                -- 移除子弹
                effectParent:removeChild(node);
                finishEffect();
            end);

            local moveTo = cc.MoveTo:create(duration, farmostPos);
            node:runAction(cc.Sequence:create(moveTo, callfunc));

            for i, delay in pairs(delayList) do
                local targetNode = targetNodes[i];

                -- 爆炸
                local function playDamage()
                    --cclog("格子(%d)爆炸", targetNode.gridData:getPos());

                    playEffect(targetNode, 1006, 0, 0);
                    targetNode:playEffect(targetNode, "animation/open_block.csb", 0.5, nil, 1);

                    if i == #delayList then
                        -- TODO 等最后一个格子爆炸后，调用一下回合事件的回调
                        UIDungeonMgr:getCurLevel():whenCombatRound();
                    end
                end

                performWithDelay(sourceNode, playDamage, delay);
            end

            AudioM.playFx("mic_launch");

            -- 尝试触发开格子
            local openInterval = 0.1;
            EventMgr.fire(event.PLAYER_MOVE, { ["interval"] = openInterval,});

            if sourceNode.gridData.state == GRID_STATE_DISABLE then
                -- 排炮失效
                sourceNode.animation:play(24);
            end
        end
    end

    performWithDelay(sourceNode, launch, prepareTime);
end

-- 播放血腥陷阱攻击动画
function playBloodTrapEffect(sourceNode, targetNodes)
    if sourceNode.gridData.state == GRID_STATE_DISABLE then
        sourceNode.item:removeAllChildren();
    end

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    local fromPos = cc.p(sourceNode:getPosition());

    local node = cc.Node:create();
    node:setPosition(fromPos);
    effectParent:addChild(node);

    -- 播放血腥陷阱触发光效
    playEffect(node, 1260, 0, -20);

    -- 屏幕震动
    performWithDelay(sourceNode, function ()
        playShakeEffect(UIDungeonMgr:getCurLevel(), 0.03, 8, 8);
    end, 0.2);

    -- 0.5秒后抛出移动事件，自动开启未开启的格子
    performWithDelay(sourceNode, function ()
        EventMgr.fire(event.PLAYER_MOVE, { ["interval"] = 0,});
    end, 0.5);

    -- 0.5秒后在已开启的格子上播放开启光效
    if type(targetNodes) == "table" and #targetNodes then
        for _, grid in ipairs(targetNodes) do
            performWithDelay(grid, function ()
                grid:playEffect(grid, "animation/open_block.csb", 0.5, nil, 1);
            end, 0.5);
        end
    end
end

-- 播放定时炸弹攻击动画
function playTimeBombEffect(sourceNode, targetNodes)
    if sourceNode.gridData.state == GRID_STATE_DISABLE then
        sourceNode.item:removeAllChildren();
    end

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    local fromPos = cc.p(sourceNode:getPosition());

    local node = cc.Node:create();
    node:setPosition(fromPos);
    effectParent:addChild(node);

    -- 播放爆炸光效
    playEffect(node, 1234, 0, -20);

    -- 屏幕震动
    performWithDelay(sourceNode, function ()
        playShakeEffect(UIDungeonMgr:getCurLevel(), 0.03, 8, 8);
    end, 0.2);

    -- 0.5秒后在已开启的格子上播放开启光效
    if type(targetNodes) == "table" and #targetNodes then
        for _, grid in ipairs(targetNodes) do
            performWithDelay(grid, function ()
                grid:playEffect(grid, "animation/open_block.csb", 0.5, nil, 1);
            end, 0.5);
        end
    end
end

-- 播放杂技彩球攻击动画
function playBallAttackEffect(sourceNode, targetNodes)
    -- 启动时间
    local prepareTime = 0.1;

    -- 滚动
    local function roll()
        -- 彩球起始位置
        local fristNode = targetNodes[1];
        local fromPos = cc.pAdd(cc.p(fristNode:getPosition()), cc.p(0, 30));

        -- 获取彩球的图标路径
        local ballPath = getLevelItemIconPath("ball");
        local shadowPath = getLevelItemIconPath("ball_shadow");

        -- 绘制彩球
        local ballImg = ccui.ImageView:create(ballPath);
        local shadowImg = ccui.ImageView:create(shadowPath);
        local node = cc.Node:create();
        local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
        effectParent:addChild(node);
        node:setPosition(fromPos);
        node:addChild(ballImg);
        node:addChild(shadowImg);

        -- 速度系数
        local speedCoef = 0.8;

        if #targetNodes > 0 then
            -- 计算到达各个节点所需的时间
            local delayList = {};
            for _, targetNode in pairs(targetNodes) do
                local toPos = cc.pAdd(cc.p(targetNode:getPosition()), cc.p(0, 30));
                local duration = cc.pGetDistance(fromPos, toPos)/(speedCoef*speed);
                table.insert(delayList, duration);

                cclog("格子：%d, delay = %f", targetNode.gridData:getPos(), duration);
            end

            -- 获取该方向上最远的目标
            local farmostNode = targetNodes[#targetNodes];
            local farmostPos = cc.pAdd(cc.p(farmostNode:getPosition()), cc.p(0, 30));
            local duration = cc.pGetDistance(fromPos, farmostPos)/(speedCoef*speed);

            local callfunc = cc.CallFunc:create(function()
                -- 移除彩球
                effectParent:removeChild(node);
                finishEffect();
            end);

            local moveTo = cc.MoveTo:create(duration, farmostPos);
            local rotate = cc.RotateBy:create(duration, 720);
            node:runAction(cc.Sequence:create(moveTo, callfunc));
            ballImg:runAction(cc.Sequence:create(rotate));

            for i, delay in pairs(delayList) do
                local targetNode = targetNodes[i];

                -- 爆炸
                local function playDamage()
                    --cclog("格子(%d)爆炸", targetNode.gridData:getPos());

                    playEffect(targetNode, 1006, 0, 0);
                    targetNode:playEffect(targetNode, "animation/open_block.csb", 0.5, nil, 1);

                    if i == #delayList then
                        -- TODO 等最后一个格子爆炸后，调用一下回合事件的回调
                        UIDungeonMgr:getCurLevel():whenCombatRound();
                    end
                end

                performWithDelay(sourceNode, playDamage, delay);
            end

            AudioM.playFx("mic_launch");

            -- 尝试触发开格子
            local openInterval = 0.2;
            EventMgr.fire(event.PLAYER_MOVE, { ["interval"] = openInterval,});
        end
    end

    performWithDelay(sourceNode, roll, prepareTime);
end

-- 播放增益效果
function playAddBuffEffect(targetNode, buffType)
    if not targetNode then
        return;
    end

    if targetNode.gridData == nil or targetNode.gridData.monster == nil then
        return;
    end

    -- 右上角出现增益图标
    local image = ccui.ImageView:create();
    image:loadTexture(getMonsterBuffIconPath(buffType));
    image:setPosition(50, 77);
    image:setOpacity(0);

    local effectScale = 1;
    local classId = targetNode.gridData.monster:query("base_class_id");

    if MonsterM.query(classId, "monster_type") == MONSTER_TYPE_BOSS then
        image:loadTexture(getBossBuffIconPath(buffType));
        image:setPosition(150, 180);
        effectScale = 1.5;

        playEffect(targetNode.barNode, 1182, -150, 230, nil, 1.5);

        local function delay()
            playEffect(targetNode, 1181, -150, 250, nil, 1.5);
        end
        performWithDelay(targetNode, delay, 0.4);
    else
        playEffect(targetNode.barNode, 1182, 0, -23);

        local function delay()
            playEffect(targetNode, 1181, 0, -30);
        end
        performWithDelay(targetNode, delay, 0.4);
    end

    targetNode.barNode:addChild(image);

    -- 先渐渐显示，然后向上飘并渐隐消失
    local fadeIn = cc.FadeIn:create(0.5);
    local delay = cc.DelayTime:create(0.2);
    local moveUp = cc.MoveBy:create(0.5, cc.p(0, 30));
    local fadeOut = cc.FadeOut:create(0.5);
    local callFunc = cc.CallFunc:create(function()
        targetNode.barNode:removeChild(image);
    end);

    image:runAction(cc.Sequence:create(fadeIn, delay, cc.Spawn:create(moveUp, fadeOut), callFunc));
end

-- 播放增益效果
function playAddDeBuffEffect(targetNode, debuffType)
    if not targetNode then
        return;
    end

    playEffect(targetNode.barNode, 1183, 0, 7);

    local function delay()
        playEffect(targetNode, 1230, 0, -20);
    end
    performWithDelay(targetNode, delay, 0.4);

    -- 右上角出现减益图标
    local image = ccui.ImageView:create();
    image:loadTexture(getMonsterBuffIconPath(debuffType));
    image:setPosition(50, 77);
    image:setOpacity(0);

    targetNode.barNode:addChild(image);

    -- 先渐渐显示，然后向下飘并渐隐消失
    local fadeIn = cc.FadeIn:create(0.5);
    local delay = cc.DelayTime:create(0.2);
    local moveUp = cc.MoveBy:create(0.5, cc.p(0, -30));
    local fadeOut = cc.FadeOut:create(0.5);
    local callFunc = cc.CallFunc:create(function()
        targetNode.barNode:removeChild(image);
    end);

    image:runAction(cc.Sequence:create(fadeIn, delay, cc.Spawn:create(moveUp, fadeOut), callFunc));
end

-- 播放怪物出现效果
function playMonsterFirstAppearEffect(targetNode)
    -- TODO 暂时用替代光效
    playEffect(targetNode, 1172, 0, 0);
end

-- 播放死亡触发效果
function playMonsterDieTriggerEffect(targetNode)
    -- TODO 暂时用替代光效
    playEffect(targetNode, 1280, 0, 0);
end

-- 震动
function playMultiFormShakeEffect(nodeList)
    local duration = 0.03;
    local offset = 4;

    local moveLeft = cc.MoveBy:create(duration, cc.p(-offset, 0));
    local moveDown = cc.MoveBy:create(duration, cc.p(0, -offset));
    local moveRight = cc.MoveBy:create(duration, cc.p(offset, 0));
    local moveUp = cc.MoveBy:create(duration, cc.p(0, offset));
    local action = cc.Sequence:create(moveLeft, moveDown, moveRight, moveUp);

    for _, node in pairs(nodeList) do
        node:runAction(action:clone());
    end
end

-- 邮戳效果
function playStampEffect(textNode, str, fontSize, callback, maskBg, nodeList)
    textNode:setString(str);
    maskBg = maskBg or "images/ui/text_mask/text_mask.png";
    local len = string.getWCharCount(str);

    resetEffectText(textNode);
    for i = 1, len do
        local effectNodeList = addEffectText(textNode, 0, 0, str, i, fontSize, maskBg);
        local function doEffect(node)
            node:setVisible(false);
            node:setScale(1.5);
            local callFunc1 = cc.CallFunc:create(function()
                node:setVisible(true);
                local oldPosY = node:getPositionY();
                node:setPositionY(40);
                local moveBy = cc.MoveBy:create(0.1 / len, cc.p(0, oldPosY - 40));
                local scaleBy = cc.ScaleTo:create(0.1 / len, 1);
                local shake = cc.CallFunc:create(function()
                    playMultiFormShakeEffect(nodeList);
                    AudioM.playFx("new_level_word");
                end);

                node:runAction(cc.Sequence:create(cc.Spawn:create(moveBy, scaleBy), shake));
            end);

            local delay = cc.DelayTime:create(1.5 / len * i);
            node:runAction(cc.Sequence:create(delay, callFunc1));
        end

        for _, effectNode in pairs(effectNodeList) do
            doEffect(effectNode);
        end
    end

    local delay = cc.DelayTime:create(2);
    local callFunc = cc.CallFunc:create(function()
        if callback then
            callback();
        end
    end);

    textNode:runAction(cc.Sequence:create(delay, callFunc));
end


-- 标题飞入
function playFlyInEffect(textNode, str, fontSize, callback, maskBg)
    textNode:setString(str);

    maskBg = maskBg or "images/ui/text_mask/text_mask.png";
    local len = string.getWCharCount(str);

    resetEffectText(textNode);
    for i = 1, len do
        local effectNodeList = addEffectText(textNode, 0, 0, str, i, fontSize, maskBg);
        local function doEffect(node)
            node:setVisible(false);
            node:setScale(1.3);
            local duration = 1.5 / len;
            local callFunc1 = cc.CallFunc:create(function()
                node:setVisible(true);
                local rotate = cc.RotateBy:create(duration, 360);
                local fadeIn = cc.FadeIn:create(duration);
                local scaleBy = cc.ScaleTo:create(duration, 1);
                node:runAction(rotate);
                node:runAction(fadeIn);
                node:runAction(scaleBy);
            end);

            local delay = cc.DelayTime:create(1 / len * i);
            node:runAction(cc.Sequence:create(delay, callFunc1));
            AudioM.playFx("new_level");
        end

        for _, effectNode in pairs(effectNodeList) do
            doEffect(effectNode);
        end
    end

    local oldPosX = textNode:getPositionX();
    local oldPosY = textNode:getPositionY();

    textNode:setPositionX(-DESIGN_WIDTH);
    local moveTo = cc.MoveTo:create(0.3, cc.p(oldPosX, oldPosY));
    local delay = cc.DelayTime:create(0.9);
    local callFunc = cc.CallFunc:create(function()
        if callback then
            callback();
        end
    end);

    textNode:runAction(cc.Sequence:create(moveTo, delay, callFunc));
end

-- 播放召唤兽出现特效
function playSummonAppearEffect(sourceNode, effectId)
    local fromPos = cc.p(sourceNode:getPosition());
    local node = cc.Node:create();
    node:setPosition(fromPos);

    playEffect(node, effectId);

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    effectParent:addChild(node);
end

-- 播放怪物逃跑特效
function playMonsterFleeEffect(sourceNode)
    local fromPos = cc.p(sourceNode:getPosition());
    local node = cc.Node:create();
    node:setPosition(fromPos);

    playEffect(node, 1320);

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    effectParent:addChild(node);
end

-- 胆小的骷髅逃跑特效
function playSkeletonFleeEffect(sourceNode)
    local fromPos = cc.p(sourceNode:getPosition());
    local node = cc.Node:create();
    node:setPosition(fromPos);

    -- 默认动画往左跑
    local perMove = -13;

    if node:getPositionX() < DESIGN_WIDTH / 2 then
        -- 在屏幕偏左，节点水平翻转，往右跑
        node:setScaleX(-node:getScaleX());
        perMove = -perMove;
    end

    local function frameEventFunc(frame)
        if frame.getEvent and frame:getEvent() == "pilor" then
            local name = frame:getNode():getName();
            local index = frame:getFrameIndex();
            node:setPositionX(node:getPositionX() + perMove);
            if node:getPositionX() > DESIGN_WIDTH
               or node:getPositionX() < 0 then
                -- 跑出屏幕外了
                node:removeAllChildren();
                -- 测试用（左右折返跑）
                --[[node:setScaleX(-node:getScaleX());
                perMove = -perMove;--]]
            end
        end
    end

    playEffect(node, 1243, 0, 0, frameEventFunc);

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    effectParent:addChild(node);
end

-- 播放精灵困在笼内动画
function playPixyLockEffect(sourceNode, dustEffectParent)
    playPixyEffect(sourceNode, dustEffectParent);
end

-- 小妖精动画
function playPixyEffect(sourceNode, dustEffectParent)
    -- 去掉节点上所有动画
    dustEffectParent:stopAllActions();
    dustEffectParent:removeAllChildren();

    local node = cc.Node:create();
    node:setPosition(100, 60);

    -- 小精灵动画
    local effectAniam = createEffectAnim(1246);
    -- 动画缓存机制，可能需要校正位置
    effectAniam.node:setPosition(cc.p(0, 0));
    effectAniam:setPositionY(effectAniam:getPositionY() - 20);
    node:addChild(effectAniam);
    local function pixyEffect()
        -- 1祯开始/30祯结束
        effectAniam:play(1, 30);
        performWithDelay(node, pixyEffect, 0.8);
    end
    pixyEffect();

    dustEffectParent:addChild(node);
end

-- 笼子打开动画
function playPrisonOpenEffect(sourceNode, dustEffectParent)
    local node = cc.Node:create();
    node:setPosition(100, 60);

    -- 笼子打开动画
    playEffect(node, 1245, -4, -30);

    dustEffectParent:addChild(node);
end

-- 小精灵飞走动画
function playPixyFleeEffect(sourceNode, dustEffectParent)
    -- 去掉节点上所有动画
    dustEffectParent:removeAllChildren();

    local node = cc.Node:create();
    node:setPosition(100, 60);

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    if DungeonInstanceM.isInInstance() then
        -- 洞穴里
        dustEffectParent:addChild(node);
    else
        effectParent:addChild(node);
        node:setPosition(cc.p(sourceNode:getPosition()));
    end

    -- 精灵飞走动画
    playEffect(node, 1246, 0, -20);

    -- 遮罩需要跟随小精灵
    local caveNode = UIDungeonMgr:getCurLevel():getCaveMaskNode();
    local delay1 = cc.DelayTime:create(1.8);
    local delay2 = cc.DelayTime:create(0.1);
    local moveBy1 = cc.MoveBy:create(0.3, cc.p(-150, 0));
    local delay3 = cc.DelayTime:create(0.8);
    local moveBy2 = cc.MoveBy:create(0.6, cc.p(300, 0));
    local moveBy3 = cc.MoveBy:create(0.3, cc.p(-150, 0));

    caveNode:runAction(cc.Sequence:create(delay1, moveBy1, delay2, moveBy2, delay3, moveBy3));
end

-- 魅魔小屋动画
function playCabinEffect(sourceNode)
    local dustEffectParent = findChildByName(sourceNode, "effect_node");
    -- 去掉节点上所有动画
    dustEffectParent:removeAllChildren();

    local uiCurLevel = UIDungeonMgr.getCurLevel();
    local imgNode = uiCurLevel:getBlackMask();

    -- 初始化黑色蒙版属性
    imgNode:setVisible(true);
    imgNode:setOpacity(0);
    imgNode:loadTexture("images/ui/common/mengban_black.png");
    imgNode:setCascadeOpacityEnabled(true);
    imgNode:setTouchEnabled(true);

    -- 图片渐入渐出
    local fadeOut = cc.FadeTo:create(1.5, 0);
    local fadeIn = cc.FadeTo:create(0.8, 230);
    local delay = cc.DelayTime:create(2);

    local callFunc = cc.CallFunc:create(function()
        imgNode:setVisible(false);
        imgNode:setOpacity(255);
        imgNode:setTouchEnabled(false);
        imgNode:loadTexture("images/ui/level/black_mask2.png");
    end);

    imgNode:runAction(cc.Sequence:create(fadeIn, delay, fadeOut, callFunc));

    -- 播放桃心光效
    performWithDelay(sourceNode, function ()
        playEffect(imgNode, 1257, AlignM.frameSize.width / 2, 70);
    end, 1);
end

-- 神灯巨人自爆
function lampGenieExplosionEffect(sourceNode)
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    local node = cc.Node:create();
    node:setPosition(cc.p(sourceNode:getPosition()));
    effectParent:addChild(node);

    playEffect(node, 1268, 0, 25);
    playEffect(sourceNode, 1269);
end

-- 获得技能特效
function pickupSkillEffect(sourceNode, targetNode, iconPath, skillName)
    if targetNode.gainItemList == nil then
        targetNode.gainItemList = {};
    end

    table.insert(targetNode.gainItemList, skillName);

    -- 目标位置先转成世界坐标
    local worldPos = targetNode:convertToWorldSpaceAR(cc.p(0, 0));

    -- 再转到相对于sourceNode的父节点的坐标
    local parentNode = sourceNode:getParent();
    local toPos = parentNode:convertToNodeSpace(worldPos);
    local fromPos = cc.p(sourceNode:getPosition());

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();

    local function callback()
        -- 卷轴图标闪一下
        playEffect(targetNode, 1106, 63, 53);
        finishEffect();

        showItemName(targetNode, TextStyleM.TEXT_COLOR_CYAN);
    end

    commonItemsFlyTo(effectParent, fromPos, toPos, iconPath, 1, callback);
end

-- 播放石柱禁魔光效
function playForbidMagicEffect(sourceNode, isPause)
    local stoneId = sourceNode.gridData.id or sourceNode.gridData.stone_id;
    local stoneType = MagicStoneM.query(stoneId, "type");
    if not stoneType then
        -- 不是石柱
        return;
    end

    -- 光效配置
    local darkEffectMap  = { 1192, 1193, 1194, 1195, 1196, 1209 };
    local lightEffectMap = { 1197, 1198, 1199, 1200, 1201, 1208 };

    -- 创建石柱动画
    local effectMap;
    if stoneType == STONE_TYPE_LIGHT then
        -- 光系石柱
        effectMap = lightEffectMap;
    else
        -- 暗系石柱
        effectMap = darkEffectMap;
    end

    local fromPos = cc.p(sourceNode:getPosition());

    -- 是否暂停
    isPause = isPause or false;

    -- 石柱模型动画
    local animation = sourceNode.item.animation;
    if not animation then
        return;
    end

    if isPause then
        animation:gotoFrameAndPause(60);
    else
        animation:play(0, 60);

        -- 光圈音效
        AudioM.playFx("ward_lifted");

        -- 由于石柱的光圈要在所有格子之上，因此不能将其放在格子上，只能放到特效节点上
        local effectParent = UIDungeonMgr.getCurLevel():getEffectParent();
        local node = cc.Node:create();
        local circleAnim = createEffectAnim(effectMap[6]);
        circleAnim:play(0, 60);
        node.circleAnim = circleAnim;
        node:setName("stone_circle_anim");
        node:setPosition(cc.pAdd(fromPos, cc.p(0, 0)));
        node:addChild(circleAnim);
        effectParent:addChild(node);
    end

    -- 播放其他修饰特效
    local function playDecaration()
        local decaNode = cc.Node:create();

        -- 小石头上下飘动
        local rockLeftNode = cc.Node:create();
        playEffect(rockLeftNode, effectMap[5]);
        decaNode:addChild(rockLeftNode);

        local rockRightNode = cc.Node:create();
        playEffect(rockRightNode, effectMap[4]);
        decaNode:addChild(rockRightNode);

        -- 闪烁特效
        local shinningNode = cc.Node:create();
        playEffect(shinningNode, effectMap[3]);
        decaNode:addChild(shinningNode);

        -- 灯光特效
        local lightNode = cc.Node:create();
        playEffect(lightNode, effectMap[2]);
        decaNode:addChild(lightNode);

        sourceNode.item.decaNode = decaNode;
        sourceNode.item:addChild(decaNode);

        local function showAlert()
            -- 显示石柱警戒
            if stoneType == STONE_TYPE_LIGHT then
                AlertStateM.setState(AlertStateM.ALERT_STATE_LIGHT_STONE);
            else
                AlertStateM.setState(AlertStateM.ALERT_STATE_DARK_STONE);
            end
        end
        performWithDelay(sourceNode, showAlert, 1.5);
    end

    -- 如果是暂离回来，则立即显示装饰特效，否则需要等播放光圈特效后才显示
    local delay = iif(isPause == true, 0, 0.3);
    performWithDelay(sourceNode, playDecaration, delay);
end

-- 播放石柱关闭特效
function playStoneCloseEffect(sourceNode)
    local stoneId = sourceNode.gridData.id or sourceNode.gridData.stone_id;
    local stoneType = MagicStoneM.query(stoneId, "type");
    if not stoneType then
        -- 不是石柱
        return;
    end

    local animation = sourceNode.item.animation;
    local decaNode = sourceNode.item.decaNode;
    if not animation then
        return;
    end

    if decaNode == nil then
        -- 修饰特效已经不存在，应该是已经播放过关闭动画了，直接停在石柱关闭动画的最后一帧
        animation:gotoFrameAndPause(102);
        return;
    end

    -- 播放石柱关闭动画
    animation:play(64, 102);

    -- 光圈音效
    AudioM.playFx("ward_aperture");

    local effectParent = UIDungeonMgr.getCurLevel():getEffectParent();
    local circleNode = effectParent:getChildByName("stone_circle_anim");
    if circleNode == nil then
        -- 光圈动画尚未创建，则先创建
        local effectId = iif(stoneType == STONE_TYPE_LIGHT, 1208, 1209);
        local fromPos = cc.p(sourceNode:getPosition());

        circleNode = cc.Node:create();
        local circleAnim = createEffectAnim(effectId);
        circleAnim:play(0, 60);
        circleNode.circleAnim = circleAnim;
        circleNode:setName("stone_circle_anim");
        circleNode:setPosition(cc.pAdd(fromPos, cc.p(0, 0)));
        circleNode:addChild(circleAnim);
        effectParent:addChild(circleNode);
    end

    circleNode.circleAnim:play(64, 102);

    -- 将修饰特效移除
    local function removeDecaration()
        if circleNode.circleAnim ~= nil then
            circleNode.circleAnim:setVisible(false);
        end

        -- 移除光圈特效
        effectParent:removeChild(circleNode);

        sourceNode.item:removeChild(decaNode);
        sourceNode.item.decaNode = nil;

        -- 移除石柱警戒
        if stoneType == STONE_TYPE_LIGHT then
            AlertStateM.clearState(AlertStateM.ALERT_STATE_LIGHT_STONE);
        else
            AlertStateM.clearState(AlertStateM.ALERT_STATE_DARK_STONE);
        end
    end

    if effectParent.stoneCloseNode == nil then
       effectParent.stoneCloseNode = cc.Node:create();
       effectParent:addChild(effectParent.stoneCloseNode);
    end

    performWithDelay(effectParent.stoneCloseNode, removeDecaration, 0.9);
end

-- 播放石柱消失特效
function playStoneDisappear(sourceNode)
    local animation = sourceNode.item.animation;

    if animation == nil then
        return;
    end

    -- 播放淡出特效
    animation:play(104, 115, false, 0.1);

    local callFunc = cc.CallFunc:create(function()
        sourceNode.item:removeAllChildren();
    end);

    local scaleUp = cc.ScaleTo:create(0.1, 1);
    local scaleDown = cc.ScaleTo:create(0.75, 0.8);
    sourceNode.item:runAction(cc.Sequence:create(scaleUp, scaleDown, callFunc));
end

-- 垂直scrollview的物品项飞入效果
function commonScrollItemFlyIn(items)
    local speed = 3000;
    local distance = DESIGN_WIDTH*2;

    local offset = DESIGN_WIDTH / #items;

    local duration = distance / speed;

    local count = 0;
    for _, item in pairs(items) do
        -- 从右边飞进来
        local oldPos = cc.p(item:getPosition());

        item:setPositionX(DESIGN_WIDTH*2);

        local delay = cc.DelayTime:create(count * (offset + count * 20) / speed);
        local moveTo = cc.EaseIn:create(cc.MoveTo:create(duration, cc.pAdd(oldPos, cc.p(-30, 0))), 2.4);
        local moveBack = cc.MoveTo:create(duration / 4, oldPos);
        item:runAction(cc.Sequence:create(delay, moveTo, moveBack));

        --AudioM.playFx("stat_fly_in");
        count = count + 1;
    end
end

-- 商店主界面商品掉落动画
function goodsDropEffect(panel, list, offsetY, delayList)
    local pageHeight = panel:getContentSize().height;

    -- 先调整下他们的位置
    local oldPosList = {};
    for _, child in pairs(list) do
        oldPosList[child:getTag()] = child:getPositionY() + offsetY;
        child:setPositionY(pageHeight);
    end

    -- 掉落动画
    local function itemDrop(item)
        local toPosY = oldPosList[item:getTag()];
        local toPos = cc.p(item:getPositionX(), toPosY);
        local scaleX = item:getScaleX();
        local scaleY = item:getScaleY();

        item:setAnchorPoint(cc.p(0.5, 0));
        local move = cc.MoveTo:create(0.2, toPos);
        local scaleDown = cc.ScaleTo:create(0.05, scaleX, scaleY-0.1);
        local scaleUp = cc.ScaleTo:create(0.05, scaleX, scaleY);
        item:runAction(cc.Sequence:create(move, scaleDown, scaleUp));
    end

    -- 随机逐个掉落
    if delayList then
        for i, item in pairs(list) do
            performWithDelay(panel, function() itemDrop(item); end, delayList[i]);
        end
    else
        for i, item in pairs(list) do
            performWithDelay(panel, function() itemDrop(item); end, 0.1 * i);
        end
    end

end

-- 炎龙刃陷阱攻击特效
function playFireBladeEffect(sourceNode, targetNodes, para)
    local para = para or {};
    if not para["borrow"] and sourceNode.icon then
        sourceNode.icon:setVisible(false);
    end

    local fromPos = cc.p(sourceNode:getPosition());

    local node = cc.Node:create();
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    effectParent:addChild(node);
    node:setPosition(fromPos);

    if not para["borrow"] then
        -- 陷阱启动特效
        playEffect(node, 1203);
    end

    -- 陷阱启动时间
    local prepareTime = 0.3;

    -- 发射
    local function launch()
        effectParent:removeChild(node);

        -- 炎龙刃格子燃烧
        playEffect(sourceNode, 1205);

        for direction, targetList in pairs(targetNodes) do

            -- 速度系数
            local speedCoef = 1.2;

            if #targetList > 0 then

                -- 计算到达各个节点所需的时间
                local delayList = {};
                for _, targetNode in pairs(targetList) do
                    local toPos = cc.p(targetNode:getPosition());
                    local duration = cc.pGetDistance(fromPos, toPos)/(speedCoef*speed);
                    table.insert(delayList, duration);
                end

                for i, delay in pairs(delayList) do
                    local targetNode = targetList[i];

                    -- 燃烧
                    local function playDamage()
                        playEffect(targetNode, 1204, 0, 0);
                    end

                    performWithDelay(sourceNode, playDamage, delay);
                end

                -- 等到到达最远格子后抛出出手事件
                local damageDelay = delayList[#delayList]
                local function prepareDamage()
                    EventMgr.fire(event.PLAYER_MOVE, { ["combatRound"] = true, });
                end
                performWithDelay(sourceNode, prepareDamage, damageDelay);
            end
        end
    end

    performWithDelay(sourceNode, launch, prepareTime);

    return 0.5;
end

-----------------------------
-- 播放建筑动画特效消失
-- @param buidlingNode   建筑格子节点
-- @param animation      建筑动画
-- @param startFrame     消失动画起始帧
-- @param endFrame       消失动画结束帧
function playAnimEffectDisappear(buidlingNode, animation, startFrame, endFrame)
    -- 整个节点逐渐缩小效果
    local imgNode = buidlingNode.item;
    local scale = imgNode:getScale();
    local scaleUp = cc.ScaleTo:create(0.1, 1*scale);
    local scaleDown = cc.ScaleTo:create(0.75, 0.8*scale);

    local callFunc = cc.CallFunc:create(function()
        if buidlingNode.gridData.state ~= GRID_STATE_DISABLE then
            -- 格子又重新刷出来了，丢弃原来的消失动作
            imgNode:setScale(1);
            return;
        end

        local function remove()
            imgNode:removeAllChildren();
        end

        if animation then
            -- 播放消失动画，播放完毕后移除所有子节点
            animation:play(startFrame, endFrame, false, 0.3, remove);
        else
            remove();
        end
    end);

    imgNode:stopAllActions();
    imgNode:runAction(cc.Sequence:create(scaleUp, scaleDown, callFunc));
end

-- 播放幽灵船攻击怪物动画
function playShipAttackMonster(sourceNode, targetNode)
    -- 炮弹从空而降
    local h = cc.Director:getInstance():getOpenGLView():getFrameSize().height;
    local fromPos = cc.p(targetNode:getPositionX(), h);

    local toPos = cc.p(targetNode:getPosition());

    -- 1.选中目标
    playEffect(targetNode, 1219);

    -- 2.子弹降落
    local bulletNode = cc.Node:create();
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    bulletNode:setPosition(fromPos);
    effectParent:addChild(bulletNode);

    local selectTime = 0.3;   -- 选择目标特效时长
    local attackTime = 0.3;   -- 幽灵船攻击动作时长

    local function fallBullet()
        -- 抛出攻击前摇结束事件，准备播放攻击动作
        EventMgr.fire(event.SHIP_PRE_ATTACK_OVER);

        local function doFall()
            -- 播放子弹特效
            playEffect(bulletNode, 1220);

            local speedCoef = 3.0;
            local duration = math.max(cc.pGetDistance(fromPos, toPos))/(speed*speedCoef);
            local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), 2.0);

            local callFunc = cc.CallFunc:create(function()
                -- 移除子弹
                effectParent:removeChild(bulletNode);

                -- 3.目标爆炸
                playEffect(targetNode, 1221);

                local function fire()
                    -- 抛出幽灵船攻击完毕事件
                    EventMgr.fire(event.SHIP_ATTACK_OVER);
                end

                -- 等爆炸特效播放完毕后抛出事件
                performWithDelay(targetNode, fire, 0.3);
            end);

            bulletNode:runAction(cc.Sequence:create(moveTo, callFunc));
        end

        performWithDelay(sourceNode, doFall, attackTime);
    end

    performWithDelay(sourceNode, fallBullet, selectTime);
end

-- 播放幽灵船攻击玩家动画
function playShipAttackHero(sourceNode, heroPos)
    -- 炮弹从空而降
    local h = cc.Director:getInstance():getOpenGLView():getFrameSize().height;
    local fromPos = cc.p(heroPos.x, h);
    local toPos = cc.pAdd(heroPos, cc.p(0, -30));
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();

    -- 1.子弹降落
    local bulletNode = cc.Node:create();
    bulletNode:setPosition(fromPos);
    effectParent:addChild(bulletNode);

    local selectTime = 0.3;   -- 选择目标特效时长
    local attackTime = 0.3;   -- 幽灵船攻击动作时长

    local function fallBullet()
        -- 抛出攻击前摇结束事件，准备播放攻击动作
        EventMgr.fire(event.SHIP_PRE_ATTACK_OVER);

        local function doFall()
            -- 播放子弹特效
            playEffect(bulletNode, 1220);

            local speedCoef = 3.0;
            local duration = math.max(cc.pGetDistance(fromPos, toPos))/(speed*speedCoef);
            local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), 2.0);

            local callFunc = cc.CallFunc:create(function()
                -- 移除子弹
                effectParent:removeChild(bulletNode);

                -- 2.目标爆炸
                local boomNode = cc.Node:create();
                boomNode:setPosition(toPos);
                playEffect(boomNode, 1221);
                effectParent:addChild(boomNode);

                local function fire()
                    effectParent:removeChild(boomNode);

                    -- 抛出幽灵船攻击完毕事件
                    EventMgr.fire(event.SHIP_ATTACK_OVER);
                end

                -- 等爆炸特效播放完毕后抛出事件
                performWithDelay(sourceNode, fire, 0.1);
            end);

            bulletNode:runAction(cc.Sequence:create(moveTo, callFunc));
        end

        performWithDelay(sourceNode, doFall, attackTime);
    end

    performWithDelay(sourceNode, fallBullet, selectTime);
end

-- 播放幽灵船施法动画
function playShipCast(sourceNode, targetNodes)
    -- 炮弹从空而降
    local h = cc.Director:getInstance():getOpenGLView():getFrameSize().height;
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();

    -- 攻击间隔
    local attackInterval = 0.2;

    -- 逐个攻击
    for i, targetNode in pairs(targetNodes) do
        local function attack()
            local fromPos = cc.p(targetNode:getPositionX(), h);

            local toPos = cc.p(targetNode:getPosition());

            -- 1.选中目标
            playEffect(targetNode, 1219);

            -- 2.子弹降落
            local bulletNode = cc.Node:create();

            bulletNode:setPosition(fromPos);
            effectParent:addChild(bulletNode);

            local function fallBullet()
                playEffect(bulletNode, 1220);

                local speedCoef = 3.0;
                local duration = math.max(cc.pGetDistance(fromPos, toPos))/(speed*speedCoef);
                local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), 2.0);

                local callFunc = cc.CallFunc:create(function()
                    -- 移除子弹
                    effectParent:removeChild(bulletNode);

                    -- 3.目标爆炸
                    playEffect(targetNode, 1221);

                    if i == #targetNodes then
                        local function fire()
                            -- 抛出幽灵船攻击完毕事件
                            EventMgr.fire(event.SHIP_CAST_OVER);
                        end

                        -- 等爆炸特效播放完毕后抛出事件
                        performWithDelay(targetNode, fire, 0.3);
                    end
                end);

                bulletNode:runAction(cc.Sequence:create(moveTo, callFunc));
            end

            performWithDelay(sourceNode, fallBullet, 0.3);
        end

        performWithDelay(sourceNode, attack, (i - 1)*attackInterval);
    end
end

-- 通用节点弹出特效
function playPopUpAppear(node, callback)
    local oldScale = node:getScale();
    local scaleUp = cc.ScaleTo:create(0.1, 1.1 * 1.25 * oldScale);
    local scaleDown = cc.ScaleTo:create(0.05, 1 * oldScale);

    if type(callback) == 'function' then
        local callFunc = cc.CallFunc:create(function()
            callback();
        end);
        node:runAction(cc.Sequence:create(scaleUp, scaleDown, callFunc));
    else
        node:runAction(cc.Sequence:create(scaleUp, scaleDown));
    end
end

-- 播放元素漩涡消失特效
function playElemSwirlDisappear(sourceNode, targetNodes)
    sourceNode.item:removeAllChildren();

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    local fromPos = cc.p(sourceNode:getPosition());

    local node = cc.Node:create();
    local circleAnim = createEffectAnim(1290);
    circleAnim:play(0, 60);
    node.circleAnim = circleAnim;
    node:setPosition(fromPos);
    node:addChild(circleAnim);
    effectParent:addChild(node);

    -- 播放引爆光效
    if type(targetNodes) == "table" and #targetNodes then
        for _, grid in ipairs(targetNodes) do
            local delay = 0.15 * math.random(3);
            performWithDelay(grid, function ()
                playEffect(grid, 1211);
            end, delay);
        end
    end

    performWithDelay(effectParent, function ()
         EventMgr.fire(event.PLAYER_MOVE, -1);
         AudioM.playFx("swirl");
    end, 0.25);
end

-- 更换对手特效
function playRelicChangeEffect(node, callback)
    local offsetY = 30;
    local oriPosX = node:getPositionX();
    local oriPosY = node:getPositionY();
    local fadeIn = cc.FadeIn:create(0.3);
    --local moveTo = cc.EaseIn:create(cc.MoveBy:create(0.3, cc.p(oriPosY, oriPosY)), 5.5);
    local moveTo = cc.MoveTo:create(0.3, cc.p(oriPosX, oriPosY));
    local callFunc = cc.CallFunc:create(function()
        if node.copyNode then
            node.copyNode:setVisible(true);
            for _, childNode in pairs(node.copyNode:getChildren()) do
                local scaleUp = cc.ScaleTo:create(0.5, 1.3);
                local fadeOut = cc.FadeOut:create(0.5);
                childNode:runAction(cc.Sequence:create(cc.Spawn:create(scaleUp, fadeOut)));
            end

            local function remove()
                 node:getParent():removeChild(node.copyNode);
            end
            performWithDelay(node:getParent(), remove, 0.5);
        end

        if callback then
            callback();
        end
    end);

    node:setPosition(cc.p(oriPosX, oriPosY + offsetY));
    node:setOpacity(0);
    node:runAction(cc.Sequence:create(cc.Spawn:create(fadeIn, moveTo), callFunc));
end

-- 扈从选择特效
function playLictorEffect(iconNode, classId, callback, type)
    local icon = ccui.ImageView:create();
    local scale = 0.66;
    if "pet" == type then
        local pet = PetM.getMyPet(classId);
        local iconIndex = pet:getIcon();
        icon:loadTexture(getHeroIconPath(iconIndex));
    else
        local iconIndex = ItemM.query(classId, "icon");
        icon:loadTexture(getItemIconPath(iconIndex));
        scale = 0.75;
    end

    local offsetY = 60;
    local iconPosX = icon:getPositionX();
    local parentSize = iconNode:getContentSize();
    icon:setPosition(parentSize.width/2, parentSize.height/2 + offsetY);

    icon:setOpacity(0);
    icon:setScale(scale);
    iconNode:addChild(icon);

    local fadeIn = cc.FadeIn:create(0.3);
    local moveTo = cc.EaseIn:create(cc.MoveBy:create(0.3, cc.p(0, -offsetY)), 5.5);
    local callFunc = cc.CallFunc:create(function()
        if callback then
            callback();
        end

        local parentSize = iconNode:getContentSize();
        --playEffect(iconNode, 1186, parentSize.width/2 - 3, parentSize.height/2 - 3);

        local curIconScale = iconNode:getScale();

        local parentSize = iconNode:getContentSize();

        local scaleUp = cc.ScaleTo:create(0.5, curIconScale * 1.3);
        local remove = cc.CallFunc:create(function()
            iconNode:removeChild(icon);
        end);

        icon:runAction(cc.Sequence:create(cc.Spawn:create(scaleUp, cc.FadeOut:create(0.5)), remove));
    end);

    icon:runAction(cc.Sequence:create(cc.Spawn:create(fadeIn, moveTo), callFunc));
end

-- 体力消耗特效
function playLifeConsumeEffect(requireLife, callback)
    local topMenu = UIMgr.getCurrentScene().topMenu;
    if topMenu == nil then
        return;
    end

    local curLife = ME.user.dbase:query("life", 0);
    local maxLife = ME.user:getMaxLife();
    local i  = 1;

    local delay = 0.8;
    local function  LifeConsume()
        local lifeNode = topMenu:getLifeNode(curLife - i + 1);
        local function frameEventFunc(frame)
            if frame.getEvent and frame:getEvent() == "pilor" then
                local name = frame:getNode():getName();
                local index = frame:getFrameIndex();

                -- 第13帧的时候，没超过上限的萝卜变灰;超过上限的干掉
                if index == 13 then
                    if curLife - i + 1 < maxLife then
                        lifeNode.grayMode = false;
                        setGrayMode(lifeNode, true);
                    else
                        lifeNode:setVisible(false);
                    end
                end
            end
        end

        -- 播放体力消耗的动画
        playEffect(lifeNode, 1222, 25, 20, frameEventFunc);

        i = i + 1;
        if i <= requireLife then
            performWithDelay(topMenu, LifeConsume, delay);
        end
    end

    LifeConsume();

    local delay = 1*requireLife;

    -- 蒙上UIInvisibleForm
    local uiForm = UIInvisibleForm.create(delay);
    UIMgr.getCurrentScene():addForm(uiForm);

    if callback then
        ME.user.dbase:set("life", curLife - requireLife);
        performWithDelay(topMenu, callback, delay);
    end
end

--
function playBubbleAnima2(parentNode, isBgFade, originScale, speed, small)
    if originScale == nil then
        originScale = 1;
    end

    -- 重复动画相关配置
    local initScale  = 1 * originScale;    -- 初始模型比例
    local transScale = 1.05 * originScale;   -- 过渡模型比例
    local finalScale = 1.1 * originScale;    -- 最终模型比例

    -- 小幅度的呼吸
    if small == true then
        initScale  = 1 * originScale;    -- 初始模型比例
        transScale = 1.02 * originScale;   -- 过渡模型比例
        finalScale = 1.05* originScale;    -- 最终模型比例
    end

    local speedCoef =  speed or 1;                  -- 速度系数

    -- 默认需要播放背景框淡入、淡出动画
    if isBgFade == nil then
        isBgFade = true;
    end

    -- 播放重复动画
    local function playRepeatAnima()
        -- 初始化
        parentNode:setVisible(true);
        parentNode:stopAllActions();
        parentNode:setScale(initScale);

        -- 开始重复动画
        -- 1.变大
        local upTime    = 0.25;
        local up2Time   = 0.25/speedCoef;
        local scaleUp   = cc.ScaleTo:create(upTime, transScale);
        local scaleUp2  = cc.ScaleTo:create(up2Time, finalScale);

        local seq1 = cc.Sequence:create(scaleUp, scaleUp2);
        local action1 = cc.Spawn:create(seq1);

        -- 2.短暂停顿
        local delayTime = 0.2/speedCoef;
        local delay = cc.DelayTime:create(delayTime);

        -- 3.变小
        local downTime  = 0.25/speedCoef;
        local down2Time = 0.25/speedCoef;
        local scaleDown = cc.ScaleTo:create(downTime, transScale);
        local scaleDown2 = cc.ScaleTo:create(down2Time, initScale);
        local seq2 = cc.Sequence:create(scaleDown, scaleDown2);
        local action2 = cc.Spawn:create(seq2);

        local sequenceNodeAction = cc.Sequence:create(action1, delay, action2);
        parentNode:runAction(cc.RepeatForever:create(sequenceNodeAction));

        -- 背景框淡入、淡出
        if isBgFade then
            local bgDialog = findChildByName(parentNode, "bg");
            local inTime  = upTime + up2Time + delayTime;   -- 淡入时间
            local outTime = downTime + down2Time;           -- 淡出时间
            local fadeIn = cc.FadeIn:create(outTime);
            local fadeOut = cc.FadeTo:create(outTime, 150);
            local sequenceBgAction = cc.Sequence:create(fadeIn, fadeOut);
            bgDialog:runAction(cc.RepeatForever:create(sequenceBgAction));
        end
    end
    playRepeatAnima();
end

-- 播放英雄复活光效
function playHeroReviveEffect()
    local heroPos = UIDungeonMgr.getCurLevel():getHeroPos();
    local effectParent = UIDungeonMgr.getCurLevel():getEffectParent();
    local node = cc.Node:create();
    playEffect(node, 1005, 0, -30);

    effectParent:addChild(node);
    node:setPosition(heroPos);
end

-- 播放BOSS出场光效
function playBossAppearEffect(gridPos)
    local effectParent = UIDungeonMgr.getCurLevel():getEffectParent();
    local node = cc.Node:create();
    local w = AlignM.frameSize.width;
    local h = AlignM.frameSize.height;
    local gridWidth  = 128;
    local gridHeight = 118;

    -- 先隐藏所有怪物
    local openMonsterGrids = UIDungeonMgr.getCurLevel():getOpenMonsterGrids();
    for i = 1, #openMonsterGrids do
        local monsterGrid = openMonsterGrids[i];
        monsterGrid:setVisible(false);
    end

    -- 当前地牢所有格子
    local allGrids = UIDungeonMgr.getCurLevel():getAllGrids();

    local function frameEventFunc(frame)
        if frame.getEvent and frame:getEvent() == "pilor" then
            local name = frame:getNode():getName();
            local index = frame:getFrameIndex();

            if index == 86 then
                -- 播放到86帧结算
                node:stopAllActions();
                node:removeAllChildren();
                -- 停止血量预警效果
                UIDungeonMgr.getCurLevel():stopHPAlertEffect();
                -- 播放后续动画
                playBossAppearEffect2(node, gridPos);
            end

            if index % 29 == 3 and index < 86 then
                -- 警告灯闪烁
                playEffect(node, 1511, gridWidth * 2.0, - gridHeight * 1.5, nil, 0.9);
            end
        end
    end

    -- 音效
    AudioM.playFx("boss_app");
    -- 红色警告
    playEffect(node, 1510, 35, 20, frameEventFunc);

    -- 血量预警效果
    UIDungeonMgr.getCurLevel():playHPAlertEffect();

    effectParent:addChild(node);
    node:setAnchorPoint(0.5, 0.5);
    if not gridPos then
        node:setPosition(allGrids[18]:getPositionX(), allGrids[18]:getPositionY());
    else
        node:setPosition(allGrids[gridPos]:getPositionX(), allGrids[gridPos]:getPositionY());
    end
end

-- 播放BOSS出场光效后续
function playBossAppearEffect2(node, gridPos)
    local w = AlignM.frameSize.width;
    local h = AlignM.frameSize.height;
    local gridWidth  = 128;
    local gridHeight = 118;

    local function frameEventFunc(frame)
        if frame.getEvent and frame:getEvent() == "pilor" then
            local name = frame:getNode():getName();
            local index = frame:getFrameIndex();

            if index == 23 then
                -- 播放到23帧结算
                node:stopAllActions();
                node:removeAllChildren();

                -- 播放后续动画
                playBossAppearEffect3(node, gridPos);
            end

            if index % 14 == 10 and index < 23 then
                -- 烟尘动画
                playEffect(node, 1513, 0, - gridHeight * 1.5);
            end
        end
    end

    -- 破土动画
    playEffect(node, 1512, 20, - gridHeight * 1.5, frameEventFunc);
end

-- 播放BOSS出场光效后续
function playBossAppearEffect3(node, gridPos)
    -- BOSS效果节点\裁剪内容
    local bossEffectParent = UIDungeonMgr.getCurLevel():getBossEffectParent();
    local bossNode = cc.Node:create();

    -- 背景烟尘节点\上移动作
    local dustNode = cc.Node:create();
    local dustNode2 = cc.Node:create();
    local dustNode3 = cc.Node:create();

    local w = AlignM.frameSize.width;
    local h = AlignM.frameSize.height;
    local gridHeight = 118;
    local gridWidth  = 128;
    local dustMove = gridHeight * 3.3 / 35;

    -- 当前地牢所有格子
    local allGrids = UIDungeonMgr.getCurLevel():getAllGrids();

    local function frameEventFunc(frame)
        if frame.getEvent and frame:getEvent() == "pilor" then
            local name = frame:getNode():getName();
            local index = frame:getFrameIndex();

            if index == 76 then
                -- 播放到76帧结算
                node:stopAllActions();
                node:removeAllChildren();
                bossNode:stopAllActions();
                dustNode:stopAllActions();
                dustNode:removeAllChildren();

                -- 播放后续动画
                playEffect(node, 1516, 0, 0);

                -- BOSS怪可见
                local function showBoss()
                    bossNode:removeAllChildren();

                    -- 显示所有怪物
                    local openMonsterGrids = UIDungeonMgr.getCurLevel():getOpenMonsterGrids();
                    for i = 1, #openMonsterGrids do
                        local monsterGrid = openMonsterGrids[i];
                        monsterGrid:setVisible(true);
                    end

                    local allGrids = UIDungeonMgr.getCurLevel():getAllGrids();
                    for _, grid in pairs(allGrids) do
                        if grid.gridData.type == GRID_TYPE_BOSS then
                            -- 抛出boss出场事件
                            EventMgr.fire(event.BOSS_SHOW, {["grid"] = grid, });
                        end
                    end
                end

                performWithDelay(node, showBoss, 0.3);
            end

            local function frameEventFunc(frame)
                if frame.getEvent and frame:getEvent() == "pilor" then
                    local name = frame:getNode():getName();
                    local index = frame:getFrameIndex();

                    if index % 2 == 0 then
                        dustNode:setPositionY(dustNode:getPositionY() + dustMove);
                    end
                end
            end

            if index % 23 == 1 and index < 76 then
                -- 背景尘土\不同节点用于调整尘土比例
                playEffect(dustNode2, 1515, 125, 0, frameEventFunc);
                playEffect(dustNode, 1515, - 90, 0);
                playEffect(dustNode3, 1515, gridWidth * 3.0 + 65, 0);
            end

            if index % 14 == 1 and index < 76 then
                -- 烟尘动画
                playEffect(node, 1513, 0, - gridHeight * 1.5);
            end
        end
    end

    -- 底框出场动画
    playEffect(bossNode, 1514, 0, - gridHeight * 1.5, frameEventFunc, 1.0);
    if not gridPos then
        bossNode:setPosition(allGrids[18]:getPositionX(), gridHeight * 1.5 - 40);
        dustNode:setPosition(allGrids[18]:getPositionX(), - 65);
    else
        bossNode:setPosition(allGrids[gridPos]:getPositionX(), gridHeight * 1.5 - 40);
        dustNode:setPosition(allGrids[gridPos]:getPositionX(), - 65);
        local offsetY = 0;
        if gridPos == 13 then
            -- TODO 暂时只允许13的
            offsetY = gridHeight;
        end
        local scale;
        if h / w > 1.5 then
            scale = w / (DESIGN_WIDTH);
        else
            -- 在宽高比小于等于1.5时（如iphone或ipad），需要略微比预定比例缩小些。 所以加了75像素
            scale = h / (DESIGN_HEIGHT + 75);
        end
        bossEffectParent:setPositionY(bossEffectParent:getPositionY() + offsetY * scale);
    end
    dustNode2:setPosition(0, 0);
    dustNode3:setPosition(0, 0);
    dustNode2:setScaleX(0.7);
    dustNode2:setScaleY(1.3);
    dustNode3:setScaleX(0.4);
    dustNode3:setScaleY(0.5)
    bossEffectParent:addChild(bossNode);
    bossEffectParent:addChild(dustNode);
    dustNode:addChild(dustNode2);
    dustNode:addChild(dustNode3);
end

-- 播放地牢内使用道具获得物品动画
function playUsePropertyGetItem(bonus)
    local uiCurLevel = UIDungeonMgr.getCurLevel();
    local fromNode = uiCurLevel:getHeroNode();

    local function delay()
        local extra = { ["fromPosOffset"] = {0, 100}};

        if bonus[1] == 1 then
            -- 物品
            local itemId = bonus[2];
            local num = bonus[3];

            local fileName = ItemM.query(itemId, "icon");
            local iconPath = getItemIconPath(fileName);

            local itemBar = uiCurLevel:getFreeItemBar();
            local iconNode = itemBar:getChildByName("icon");
            local textNode = itemBar:getChildByName("num");
            local total = ItemM.getAmount(ME.user, itemId);
            gainSpecialItemEffect(fromNode, iconNode, iconPath, textNode, total, num, extra);
            AudioM.playFx("pick_goods");
        elseif bonus[1] == 2 then
            -- 属性
            if bonus[2] == "money" then
                AudioM.playFx("pick_money");
                local itemBar = uiCurLevel:getFreeItemBar();
                local iconNode = itemBar:getChildByName("icon");
                local textNode = itemBar:getChildByName("num");
                local total = ME.user.dbase:query("money", 0);
                local iconPath = getSmallAttribIconPath("money");
                gainSpecialItemEffect(fromNode, iconNode, iconPath, textNode, total, bonus[3], extra);
            elseif bonus[2] == "gem" then
                AudioM.playFx("pick_money");
                local itemBar = uiCurLevel:getFreeItemBar();
                local iconNode = itemBar:getChildByName("icon");
                local textNode = itemBar:getChildByName("num");
                local total = ME.user.dbase:query("gem", 0);
                local iconPath = getSmallAttribIconPath("gem");
                gainSpecialItemEffect(fromNode, iconNode, iconPath, textNode, total, bonus[3], extra);
            end
        end
    end
    performWithDelay(fromNode, delay, 1);
end

-- 播放道具使用动画
function playPropertyEffect(effectId)
    local effect = effectId or 41;
    local heroPos = UIDungeonMgr.getCurLevel():getHeroPos();
    local effectParent = UIDungeonMgr.getCurLevel():getEffectParent();
    local node = cc.Node:create();
    playEffect(node, effect, 0, -30);
    effectParent:addChild(node);
    node:setPosition(heroPos);
end

-- 刚进入地牢时播放
function playSpInitEffect(targetNode, pos, textNode, iconNode, addValue)
    local function delay()
        local iconPath = "images/ui/level/item/search_point1.png";

        -- 图标飞入到菜单右上角探索点处
        local originToPosX = iconNode:getPositionX();
        local originToPosY = iconNode:getPositionY();

        -- 目标位置先转成世界坐标
        local worldPos = iconNode:convertToWorldSpaceAR(cc.p(0, 0));

        -- 再转到相对于sourceNode的父节点的坐标
        local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
        local toPos = effectParent:convertToNodeSpace(worldPos);

        local fromPos = cc.pAdd(pos, cc.p(0, 100) );

        local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / (speed * 2);

        local icon = ccui.ImageView:create();
        icon:loadTexture(iconPath);
        icon:setPosition(fromPos);
        icon:setLocalZOrder(10000);

        local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();

        local callfunc = cc.CallFunc:create(function()
            effectParent:removeChild(icon);

            ME.user.dbase:deleteTemp("reduceSp");

            local curSp = ME.user:queryAttrib("search_point");
            textNode:setString(tostring(curSp));

            -- 探索点图标闪一下
            playEffect(iconNode, 1105, 26, 18);
            commonTextEffect2(textNode, TextStyleM.TEXT_COLOR_BRIGHT_YELLOW);
        end);

        local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), 2.0);
        local rotate = cc.RotateBy:create(duration, 180);
        local scaleTo = cc.ScaleTo:create(duration, 0.5);
        icon:runAction(cc.Sequence:create( cc.Spawn:create(moveTo,rotate,scaleTo), callfunc));
        effectParent:addChild(icon);
    end
    performWithDelay(targetNode, delay, 1);
end

-- 播放神龙转圈的光效
function playDragonBallEffect()
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    local parentSize = effectParent:getContentSize();
    local node = cc.Node:create();
    node:setPosition(cc.p(parentSize.width/2, parentSize.height/2));

    local function callback()
        effectParent:removeChild(node);
    end

    local effectAnim = createEffectAnim(1210);
    effectAnim:play(0, 60, false, 0.45, callback);

    node:addChild(effectAnim);
    effectParent:addChild(node);
end

-- 播放神龙传送光效
function playDragonDeliverEffect()
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    local parentSize = effectParent:getContentSize();
    local node = cc.Node:create();
    node:setPosition(cc.p(parentSize.width/2, parentSize.height/2));

    playEffect(node, 1516);

    effectParent:addChild(node);

    local function callback()
        effectParent:removeChild(node);
    end
    performWithDelay(node, callback, 2);
end

-----------------------
-- 播放冈布奥对话后获得物品
-- @param bonus 奖励
-- @param msg   提示语
function playGainItemByHeroSpeak(bonus, msg)
    -- 播放捡到效果
    local uiCurLevel = UIDungeonMgr.getCurLevel();
    local allGrids = uiCurLevel:getAllGrids();
    local itemBar = uiCurLevel:getFreeItemBar();
    local effectNode = uiCurLevel:getEffectParent();
    local iconNode = itemBar:getChildByName("icon");
    local textNode = itemBar:getChildByName("num");
    local fileName = ItemM.query(bonus[2], "icon");
    local iconPath = getItemIconPath(fileName);

    -- 从第25格飞入
    local arr = bonus;
    if #bonus > 0 and type(bonus[1]) == "number" then
        arr = { bonus, };
    end
    for _, b in pairs(arr) do
        local total = ItemM.getAmount(ME.user, b[2]);
        local num = b[3];
        gainSpecialItemEffect(allGrids[25], iconNode, iconPath, textNode, total, num);
    end

    if type(msg) == 'string' then
        -- 飘字
        alert(msg);
    end
end

-- 播放关卡锁头爆开的特效
function playAreaLockOpenEffect(effectNode)
    playEffect(effectNode, 1249, 22, 30);
end

-- 播放木牌刷新的动画
function playBoardTransformEffect(sourceNode, callback)
    local node = cc.Node:create();
    local fromPos = cc.p(sourceNode:getPosition());
    node:setPosition(fromPos);
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();


    local function frameEventFunc(frame)
        if frame.getEvent and frame:getEvent() == "pilor" then
            local name = frame:getNode():getName();
            local index = frame:getFrameIndex();

            if index == 60 then
                callback();
                node:removeAllChildren();
            end
        end
    end

    playEffect(node, 1239, -6, 5, frameEventFunc);
    effectParent:addChild(node);
end

-- 播放木牌倒掉的动画
function playBoardBrokenEffect(sourceNode, callback)
    local node = cc.Node:create();
    local fromPos = cc.p(sourceNode:getPosition());
    node:setPosition(fromPos);
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();

    local function frameEventFunc(frame)
        if frame.getEvent and frame:getEvent() == "pilor" then
            local name = frame:getNode():getName();
            local index = frame:getFrameIndex();

            if index == 25 and type(callback) == 'function' then
                callback();
                node:removeAllChildren();
            end
        end
    end

    playEffect(node, 1238, 120, -5, frameEventFunc);
    effectParent:addChild(node);
end

-- 播放奇怪的祭坛出现的动画
function playOddAltarAppearEffect(sourceNode, callback, altarEffectParent, dustEffectParent)
    local node = cc.Node:create();
    local node2 = cc.Node:create();
    node:setPosition(cc.p(70, 50));
    node2:setPosition(cc.p(100, 55));
    node:setAnchorPoint(0.5, 0.5);

    -- 木牌动画
    AudioM.playFx("tiro_altar");
    playBoardBrokenEffect(sourceNode, nil);

    local function frameEventFunc(frame)
        if frame.getEvent and frame:getEvent() == "pilor" then
            local name = frame:getNode():getName();
            local index = frame:getFrameIndex();

            if index == 77 and type(callback) == 'function' then
                callback();
            end

            if index == 79 then
                node2:stopAllActions();
                node2:removeAllChildren();
                -- 激活光效
                playOddAltarActiveEffect(sourceNode, nil)
            end

            if index % 14 == 1 and index < 79 then
                -- 烟尘动画
                playEffect(node2, 1256, 0, -12);
            end
        end
    end

    local function delay()
        local function altarEffect()
            playEffect(node, 1240, 24.5, 3, frameEventFunc);
        end
        performWithDelay(node, altarEffect, 0.35);
        playOddAltarAppearEffec2(node2);
    end

    -- 延迟播放祭坛出现
    performWithDelay(node, delay, 0.3);

    altarEffectParent:addChild(node);
    dustEffectParent:addChild(node2);
end

-- 播放奇怪的祭坛破土动画
function playOddAltarAppearEffec2(node)
    local w = AlignM.frameSize.width;
    local h = AlignM.frameSize.height;
    local gridWidth  = 128;
    local gridHeight = 118;

    local function frameEventFunc(frame)
        if frame.getEvent and frame:getEvent() == "pilor" then
            local name = frame:getNode():getName();
            local index = frame:getFrameIndex();

            if index == 23 then
                node:stopAllActions();
                node:removeAllChildren();
            end

            if index % 14 == 10 and index < 23 then
                -- 烟尘动画
                playEffect(node, 1256, 0, -12);
            end
        end
    end

    -- 破土动画
    playEffect(node, 1512, 200, -475, frameEventFunc, 0.4);
end

-- 播放奇怪的祭坛激活的动画
function playOddAltarActiveEffect(sourceNode, callback)
    local node = cc.Node:create();
    local fromPos = cc.p(sourceNode:getPosition());
    node:setPosition(fromPos);
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();

    local function frameEventFunc(frame)
        if frame.getEvent and frame:getEvent() == "pilor" then
            local name = frame:getNode():getName();
            local index = frame:getFrameIndex();

            if index == 79 then
                effectParent:removeChild(node);

                if type(callback) == 'function' then
                    callback();
                end
            end
        end
    end

    playEffect(node, 1241, -3, 4, frameEventFunc);
    effectParent:addChild(node);
end

-- 播放能量条动画
function playGridEnergyEffect(sourceNode)
    local monster = sourceNode.gridData.monster;
    if not monster then
        return;
    end

    local energyNode    = findChildByName(sourceNode.node, "energy");
    local curEnergyIcon = findChildByName(sourceNode.node, "energy/cur_energy");
    local textLable     = findChildByName(sourceNode.node, "energy/text");
    local curEnergy     = monster.dbase:query("energy") or 0;
    local classId       = monster.classId;
    local dbase         = MonsterM.query(classId, "dbase");
    local maxEnergy     = dbase["max_energy"] or 0;
    local oldScale      = curEnergyIcon:getScaleX();
    local lastEnergy    = energyNode.lastEnergy or 0;

    if monster.dbase:query("max_energy") then
        maxEnergy = monster.dbase:query("max_energy");
    end

    local first = monster.dbase:query("energy_first") or true;
    if lastEnergy == curEnergy and not first then
        return;
    end
    energyNode.lastEnergy = curEnergy;
    monster.dbase:set("energy_first", false);

    -- 能量值文本
    textLable:setString(curEnergy .. " / " .. maxEnergy);

    local function tryToPlayGridEnergyEffect( ... )
        -- 进度条变化结束回调，用于标记当前不在播放状态
        local callFunc = cc.CallFunc:create(function()
            sourceNode.isPlaying = false;
        end);

        -- 进度条比例变化
        local scaleDown = cc.ScaleTo:create(0.3, curEnergy / maxEnergy * 1.1, curEnergyIcon:getScaleY());

        if not sourceNode.isPlaying then
            sourceNode.isPlaying = true;
            curEnergyIcon:runAction(cc.Sequence:create(scaleDown, callFunc));
        else
            performWithDelay(sourceNode, tryToPlayGridEnergyEffect, 0.01);
        end
    end

    tryToPlayGridEnergyEffect();
    playEnergyIncreaseEffect(sourceNode);
end

-- 播放能量条增长光效
function playEnergyIncreaseEffect(sourceNode)
    local monster       = sourceNode.gridData.monster;
    local energyNode    = findChildByName(sourceNode.node, "energy");
    local curEnergyIcon = findChildByName(sourceNode.node, "energy/cur_energy");
    local textLable     = findChildByName(sourceNode.node, "energy/text");
    local curEnergy     = monster.dbase:query("energy") or 0;
    local classId       = monster.classId;
    local dbase         = MonsterM.query(classId, "dbase");
    local maxEnergy     = dbase["max_energy"] or 0;
    local lastEnergyChange    = energyNode.lastEnergyChange or 0;

    -- 减少或者不变的，不需要能量条增长光效
    if curEnergy <= lastEnergyChange then
        energyNode.lastEnergyChange = curEnergy;
        return;
    end

    -- 计算出当前能量条右侧的坐标和能量条变化后的右侧坐标
    local posX = curEnergyIcon:getPositionX() - 5;
    local posY = curEnergyIcon:getPositionY();
    local toPosX = posX + curEnergyIcon:getContentSize().width * curEnergy / maxEnergy * 1.1;

    posX = posX + curEnergyIcon:getContentSize().width * lastEnergyChange / maxEnergy * 1.1;

    energyNode.lastEnergyChange = curEnergy;
    local toPos = cc.p(toPosX, posY);

    local function tryToPlayEnergyIncreaseEffect( ... )
        -- 创建一个图片节点加载能量条进度变化效果图片
        local icon = ccui.ImageView:create();
        icon:setName("energy_effect");
        icon:loadTexture(getCombatStatIconPath("energy_effect"));
        icon:setPosition(cc.p(posX, posY));
        icon:setLocalZOrder(10000);

        -- 进度变化结束后删除效果图片节点
        local callfunc = cc.CallFunc:create(function()
            energyNode:removeChild(icon);
        end);

        -- 移动效果图片
        local moveTo = cc.MoveTo:create(0.3, toPos);

        if not findChildByName(energyNode, "energy_effect") then
            -- 移动效果图片
            local moveTo = cc.MoveTo:create(0.3, toPos);
            icon:runAction(cc.Sequence:create(moveTo, callfunc));

            energyNode:addChild(icon);
        else
            performWithDelay(sourceNode, tryToPlayEnergyIncreaseEffect, 0.01);
        end
    end

    tryToPlayEnergyIncreaseEffect();
end

------------------------------------
-- 播放发言框特效（发言框带箭头）
-- @param pos          格子位置
-- @param framePos     发言框的坐标
-- @param content      内容
-- @param frameType    发言框类型
-- @param surviveTime  发言框存活时间
function playRoleTalk(pos, framePos, content, frameType, surviveTime)
    local talkEffectParent = UIDungeonMgr.getCurLevel():getTalkEffectParent();
    local node = talkEffectParent:getChildByName("talk" .. pos);

    -- 如果节点已经存在，则先移除，否则重新创建一个
    if node then
        node:removeAllChildren()
    else
        node = cc.Node:create();
        node:setName("talk" .. pos);
        node:setPosition(framePos);
        talkEffectParent:addChild(node);
    end

    createWordsFrame(node, content, frameType, surviveTime, 0);
end

--------------------------------
-- 播放发言框特效（发言框是对称的且不带箭头）
-- @param pos          格子位置
-- @param framePos     发言框的坐标
-- @param content      内容
-- @param surviveTime  发言框存活时间
function playRoleTalk2(pos, framePos, content, surviveTime)
    local talkEffectParent = UIDungeonMgr.getCurLevel():getTalkEffectParent();
    local node = talkEffectParent:getChildByName("talk" .. pos);

    -- 如果节点已经存在，则先移除，否则重新创建一个
    if node then
        node:removeAllChildren()
    else
        node = cc.Node:create();
        node:setName("talk" .. pos);
        node:setPosition(framePos);
        talkEffectParent:addChild(node);
    end

    createWordsFrame2(node, content, surviveTime);
end

-- 移除角色发言
function removeRoleTalk(pos)
    local talkEffectParent = UIDungeonMgr.getCurLevel():getTalkEffectParent();
    local node = talkEffectParent:getChildByName("talk" .. pos);

    if not node then
        -- 本来就不在发言
        return;
    end

    talkEffectParent:removeChild(node);
end

-- 播放雕像献祭升级光效
function playEvilSumitBonusEffect(grid, classify, upgradeLevel, bonusList)
    local demonInfo = EvilStatueM.getStatueInfo(classify);
    local curLevel = demonInfo.cur_level;
    local delayTime = 0;

    for index = 1, upgradeLevel do
        local bonus = bonusList[index];
        local level = curLevel - upgradeLevel + index;
        local data = EvilStatueM.queryStatue(classify, level);
        local bonusDesc = EvilStatueM.getStatueBonusDesc(classify, level);
        local name = data["name"] or "";

        local function bonusEffect()
            -- "%s的信仰提升了！"
            alert(string.format(getLocStr("submit_upgrade"), name));
            alert(bonusDesc, 1);

            playEffect(grid, 1261, -18, 140, nil, 1.3);
            playEffect(grid, 1262, -20, 120, nil, 1.3);
            grid:onBuildingBonus({ ["bonus"] = bonus, }, true);
        end

        performWithDelay(grid, bonusEffect, delayTime);

        delayTime = delayTime + math.max(2.5, 1.5 + 0.5 * #bonus);
    end

    return delayTime;
end

-- 播放魔神雕像献祭升级光效
function playDemonSumitBonusEffect(grid, classify, upgradeLevel, bonusList)
    local demonInfo = DemonStatueM.getDemonInfo(classify);
    local curLevel = demonInfo.cur_level;
    local delayTime = 0;

    for index = 1, upgradeLevel do
        local bonus = bonusList[index];
        local level = curLevel - upgradeLevel + index;
        local data = DemonStatueM.queryStatue(classify, level);
        local bonusDesc = DemonStatueM.getStatueBonusDesc(classify, level);
        local name = data["name"] or "";

        local function bonusEffect()
            -- "%s的信仰提升了！"
            alert(string.format(getLocStr("submit_upgrade"), name));
            alert(bonusDesc, 1);
            -- "获得了黑夜女士雕像的奖励！"
            -- alert(string.format(getLocStr("submit_bonus"), name), 0.7);

            playEffect(grid, 1261, -18, 140, nil, 1.3);
            playEffect(grid, 1262, -20, 120, nil, 1.3);
            grid:onBuildingBonus({ ["bonus"] = bonus, }, true);
        end

        performWithDelay(grid, bonusEffect, delayTime);

        delayTime = delayTime + math.max(2.5, 1.5 + 0.5 * #bonus);
    end

    return delayTime;
end

-- 移除攻击光效节点
function removeAttackEffectNode()
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    local effectChild = findChildByName(effectParent, "attack_effect");
    if effectChild then
        effectParent:removeChild(effectChild);
    end
end

-- 播放怪物攻击光效
function playMonsterAttackEffect(grid, classId, skillId, effectNode)
    if type(skillId) ~= "number" or skillId <= 0 then
        return;
    end

    local monsterDisplay = MonsterDisplayM.getMonsterSkillDisplay(classId, skillId);

    if not monsterDisplay or #table.keys(monsterDisplay) <= 0 then
        return;
    end

    local effectId = monsterDisplay.effect or 0;
    local offset_x = monsterDisplay.offset_x or 0;
    local offset_y = monsterDisplay.offset_y or 0;
    local effect_delay = monsterDisplay.effect_delay or 0;

    local effectParent = effectNode or UIDungeonMgr:getCurLevel():getEffectParent();
    local node = cc.Node:create();
    local effectChild = findChildByName(effectParent, "attack_effect");
    if effectChild then
        effectParent:removeChild(effectChild);
    end

    node:setName("attack_effect");
    effectParent:addChild(node);

    -- 配置了打击玩家，那么打击玩家
    local worldPos, nodePos;
    local para = monsterDisplay.para;
    if para["hit_user"] == 1 then
        local uiForm = UIMgr:getCurrentScene():getFormByName("UIDungeonMain");
        local monsterNode = findChildByName(uiForm.node, "CT2/model_node");
        worldPos = monsterNode:convertToWorldSpaceAR(cc.p(0, 0));
        nodePos = effectParent:convertToNodeSpace(worldPos);
    else
        worldPos = grid:convertToWorldSpaceAR(cc.p(offset_x, offset_y));
        nodePos = effectParent:convertToNodeSpace(worldPos);
    end

    node:setPosition(nodePos);

    if type(effectId) ~= "number" or effectId <= 0 then
        return;
    end

    local function playExtraEffect()
        -- 播放额外光效
        playEffect(node, effectId);
    end

    if effect_delay > 0 then
        performWithDelay(node, playExtraEffect, effect_delay);
    else
        playExtraEffect();
    end
end

-- 播放怪物受到伤害特效
function playMonsterDamageEffect(grid, classId, skillId, effectNode)
    local monsterDisplay = MonsterDisplayM.getMonsterSkillDisplay(classId, skillId);

    if not monsterDisplay or #table.keys(monsterDisplay) <= 0 then
        return;
    end

    local effectId = monsterDisplay.effect or 0;
    local effect_delay = monsterDisplay.effect_delay or 0;

    if type(effectId) ~= "number" or effectId <= 0 then
        return;
    end

    local offsetX = iif(monsterDisplay.offset_x == "", 0, monsterDisplay.offset_x);
    local offsetY = iif(monsterDisplay.offset_y == "", 0, monsterDisplay.offset_y);

    local function playExtraEffect()
        -- 播放额外光效
        playEffect(grid.node, effectId, offsetX, offsetY);
    end

    if effect_delay > 0 then
        performWithDelay(node, playExtraEffect, effect_delay);
    else
        playExtraEffect();
    end
end

-- 飞艇启动特效
function palyAirShipStarEffect(airShipNode)
    -- 播放飞艇残骸飞出地面效果
    -- playEffect();

    -- 设置指引进度为1
    NewAirShipM.setGuideProgress(1);
end

-- 播放圣棺出现动画
function playCoffineAppear(sourceNode)
    local effectParent = UIDungeonMgr.getCurLevel():getEffectParent();

    local fromPos = cc.p(sourceNode:getPosition());
    local node = cc.Node:create();
    node:setPosition(fromPos);
    effectParent:addChild(node);

    local function frameEventFunc(frame)
        if frame.getEvent and frame:getEvent() == "pilor" then
            local name = frame:getNode():getName();
            local index = frame:getFrameIndex();

            if index == 22 then
                -- 创建一个panel用于裁切
                local layout = ccui.Layout:create();
                local width = 108;
                local height = 127;
                layout:setContentSize(cc.size(width, height));
                layout:setAnchorPoint(0.5, 0.5);
                layout:setPosition(cc.pAdd(fromPos, cc.p(2, 42)));
                layout:setClippingEnabled(true);
                effectParent:addChild(layout);

                -- 棺木上移
                local node2 = cc.Node:create();
                node2:setPosition(cc.p(width/2, height/2));
                layout:addChild(node2);
                playEffect(node2, 1272, -0.2, -31.4);
            end

            if index == 81 then
                -- 最后一帧，将建筑图标显示处理
                sourceNode.icon:setVisible(true);
            end
        end
    end

    -- 门打开
    playEffect(node, 1273, 2, 11, frameEventFunc);

end

-- 播放出战英雄虚影效果
function playActivePetShadowEffect(iconId)
    local uiForm = UIMgr:getCurrentScene():getFormByName("UIDungeonMain");
    if not uiForm then
        return;
    end

    -- 把播放位置的坐标系转换到effectParent下面
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    local monsterNode = findChildByName(uiForm.node, "CT2/model_node");
    local worldPos = monsterNode:convertToWorldSpaceAR(cc.p(0, 0));
    local sourcePos = effectParent:convertToNodeSpace(worldPos);

    -- 预先放置图片
    local imageList = {};
    local function createImage(index)
        local image = ccui.ImageView:create();

        image:loadTexture(getHeroIconPath(iconId));
        image:setAnchorPoint(cc.p(0.5, 0.5));
        image:setScale(1.5);
        image:setOpacity(255 * 0.9 / index);
        effectParent:addChild(image);
        image:setPosition(sourcePos);
        table.insert(imageList, image);
    end

    createImage(1);
    createImage(2);

    -- 英雄位置出现一个虚影，放大
    local index = 1;
    local function playEffect()
        local image = imageList[index];

        local duration = 0.5;
        local scaleUp = cc.ScaleTo:create(duration, 1.8 * image:getScale());
        local fadeOut = cc.FadeTo:create(duration, 0);
        local moveUp = cc.MoveBy:create(duration, cc.p(0, 60));
        local callFunc = cc.CallFunc:create(function()
            image:removeFromParent();
        end);
        image:runAction(cc.Sequence:create(cc.Spawn:create(scaleUp, fadeOut, moveUp), callFunc));

        index = index + 1;
    end

    playEffect();
    performWithDelay(monsterNode, playEffect, 0.2);
end

-- 播放冈布奥虚影效果
function playSlimeVirtualShadowEffect(petId, targetPos, skillId)
    local effectParent = UIMgr.getCurrentScene():getEffectRoot();

    if effectParent.SKTNode == nil then
        effectParent.SKTNode = cc.Node:create();
        effectParent:addChild(effectParent.SKTNode);
    end

    effectParent.SKTNode:stopAllActions();
    effectParent.SKTNode:removeAllChildren();

    local width = AlignM.frameSize.width;
    local height = AlignM.frameSize.height;

    -- 预先放置图片
    local imageList = {};
    local function createImage(index)
        local image = ccui.ImageView:create();
        local iconIndex = PetM.getIcon(petId);
        image:loadTexture(getHeroIconPath(iconIndex));
        image:setAnchorPoint(cc.p(0.5, 0.5));
        image:setPosition(width / 2, height / 2);
        image:setScale(1.5 * width / DESIGN_WIDTH);
        image:setOpacity(255 * 0.9 / index);
        effectParent.SKTNode:addChild(image);
        table.insert(imageList, image);
    end

    createImage(1);
    createImage(2);

    -- 英雄位置出现一个虚影，放大
    local index = 1;
    local function playEffect()
        local image = imageList[index];

        local duration = 0.5;
        local scaleUp = cc.ScaleTo:create(duration, 2.5 * image:getScale());
        local fadeOut = cc.FadeTo:create(duration, 0);
        local moveUp = cc.MoveBy:create(duration, cc.p(0, 60));
        local callFunc = cc.CallFunc:create(function()
            image:removeFromParent();
        end);
        image:runAction(cc.Sequence:create(cc.Spawn:create(scaleUp, fadeOut, moveUp), callFunc));

        if index == 2 and nil ~= effectTempInfo[skillId] and effectTempInfo[skillId]["isRetain"] == false then
            effectTempInfo[skillId] = nil;
        end

        index = index + 1;
    end

    playEffect();
    performWithDelay(effectParent.SKTNode, playEffect, 0.2);
end

-- 播放特技发动效果
-- 规则：1.必定触发的专属技能，仅在每层首次触发的时候才会播放触发效果，提示语需要保留
--       2.概率触发的专属技能，保持原状
--       3.群攻技能触发效果时（如魔法师冈布奥施放死亡波纹），播放一次就好
--       4.某些特殊的专属技虚影和飘字在每层只播一次，把propId放在noAlertList里统一处理
--       5.在prop.csv中的effect_id列配置了项，播放专属的出发光效，否则播放默认的虚影效果
local noAlertList = {514};
function playSpecialSkillTriggerEffect(skillId, petId, propId, pos)
    if not UIDungeonMgr:getCurLevel() or
        (type(pos) == "number" and pos <= 0) then
        -- 界面还没创建好
        return;
    end

    local dungeonId = DungeonM.getDungeonId();
    local curFloor = UIDungeonMgr:getCurLevel().curFloor;
    local applyProb = PropM.query(propId, "is_certain");
    local effectIds = PropM.query(propId, "effect_id");

    effectTempInfo = effectTempInfo or {};

    -- 是否在当前层
    local function isInCurFloor()
        if effectTempInfo[skillId]
            and effectTempInfo[skillId]["dungeonId"] == dungeonId and effectTempInfo[skillId]["curFloor"] == curFloor then
            return true;
        end
        return false;
    end

    -- 如果有对应技能的光效正在播放，那么return
    if isInCurFloor() and effectTempInfo[skillId] and effectTempInfo[skillId]["isPlaying"] == true then
        return;
    -- 如果检测到层数或者地牢发生变化，那么先清空之前的数据
    elseif effectTempInfo[skillId]
        and (effectTempInfo[skillId]["dungeonId"] ~= dungeonId or effectTempInfo[skillId]["curFloor"] ~= curFloor) then
        effectTempInfo = {};
    end

    if applyProb == 1 then
        if nil == effectTempInfo[skillId] then
            effectTempInfo[skillId] = {
                ["skillId"] = skillId,
                ["dungeonId"] = dungeonId,
                ["curFloor"] = curFloor,
                ["applyProb"] = applyProb,
                ["isRetain"] = true,
                ["isEffectPlay"] = false,
                ["isInTemp"] = false,
                ["isPlaying"] = true,
                ["effectIds"] = effectIds,
        }
        else
            effectTempInfo[skillId]["isPlaying"] = true;
        end
    else
        effectTempInfo[skillId] = {
            ["skillId"] = skillId,
            ["dungeonId"] = dungeonId,
            ["curFloor"] = curFloor,
            ["applyProb"] = applyProb,
            ["isRetain"] = false,
            ["isEffectPlay"] = false,
            ["isInTemp"] = false,
            ["isPlaying"] = true,
            ["effectIds"] = effectIds,
        }
    end

    local skillName = SkillM.query(skillId, "name");
    local petName = PetM.query(petId, "name");
    print(string.format("特技发动，<%s>冈布奥发动了<%s>特技", petName, skillName));

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();

    -- 获取中间那个格子的坐标
    local targetPos = cc.p(UIDungeonMgr.getCurLevel():getAllGrids()[18]:getPosition());
    local finalPos = effectParent:convertToNodeSpace(cc.pAdd(targetPos, cc.p(-15, 64)));

    -- 飘字显示
    -- 某些专属技每层只播一次
    local msg = string.format(getLocStr("special_skill_trigger_text"), skillName)
    for _, c in pairs(noAlertList) do
        if (propId == c and isInCurFloor() and effectTempInfo[skillId]["isEffectPlay"] == false)
            or propId ~= c then
            alert(msg);
        end
    end

    -- 根据条件播放特效
    local function playSlimePropEffect()
        if type(effectTempInfo[skillId]["effectIds"]) == "table" and type(pos) == "number" then
            local delay = 0;
            local grid = UIDungeonMgr.getCurLevel():getAllGrids()[pos];

            local i = 1;
            local key = "effect" .. i;
            while nil ~= effectIds[key] do
                local info = effectIds[key];
                local effectId = info["effect_id"];
                delay = delay + info["delay"];

                -- 顺序播放
                local offset = info["offset"] or {0, 22};
                performWithDelay(grid.node, function () playEffect(grid.node, effectId, offset[1], offset[2]); end, delay);

                i = i + 1;
                key = "effect" .. i;
            end

            if nil ~= effectTempInfo[skillId] and effectTempInfo[skillId]["isRetain"] == false then
                effectTempInfo[skillId] = nil;
            end
        else
            playSlimeVirtualShadowEffect(petId, finalPos, skillId);
        end
    end

    -- 如果是必定触发技能，那么每层只播放一次特效
    if applyProb == 1 then
        -- 如果在副本或者夹层中，也要走一次规则判断
        if DungeonInstanceM.isInInstance() or UnrealDungeonM.isInUnreal() then
            if effectTempInfo[skillId]["isInTemp"] == false then
                playSlimePropEffect();
                effectTempInfo[skillId]["isInTemp"] = true;
            end
        end

        -- 普通地牢内
        if isInCurFloor() and effectTempInfo[skillId]["isEffectPlay"] == true then

        else
            playSlimePropEffect();
            effectTempInfo[skillId]["isEffectPlay"] = true;
        end
    else
        playSlimePropEffect();
    end

    -- 标记已播放完成
    local effectParent = UIMgr.getCurrentScene():getEffectRoot();
    if effectParent.SKTNode then
        performWithDelay(effectParent.SKTNode, function ()
            if nil ~= effectTempInfo[skillId] then
                effectTempInfo[skillId]["isPlaying"] = false;
            end
        end, 0.3);
    end
end

-- 播放漫画特效
-- cartoonId 对应cartoon.csv中的ID
-- callback  回调函数
function playCartoonEffect(cartoonId, callback)
    require "game/ui/form/cartoon/UICartoon";
    local uiForm = UICartoon.create(cartoonId, callback);
    UIMgr.getCurrentScene():addForm(uiForm);
end

-- 创建一个飞艇
-- 如果有配置飞艇特效，则创建一个光效，否则创建一个静态图片
function createAirShip(parent, shipId)
    local airShipConfig =  NewAirShipM.query(shipId);

    -- 如果配置了飞艇的飞行特效，那么创建一个节点挂着特效
    if type(airShipConfig["fly_effect"]) == "number" then
        local node = cc.Node:create();
        node:setName("airship");

        parent:addChild(node);

        playEffect(node, airShipConfig["fly_effect"]);

        -- 之后的消失效果会修改透明度，这边重置下
        local children = node:getChildren();
        local len = table.getn(children)
        for i = 1, len do
            local child = children[i];
            child:setOpacity(255);
        end

        return node;
    else
        local image = ccui.ImageView:create();
        image:loadTexture(getAirShipImgPath(airShipConfig["icon"]));
        image:setName("airship");

        parent:addChild(image);

        return image;
    end
end

-- 石化单个目标
function petrifiedTarget(pos, noEffect, content)
    local function playStone(node)
        -- 置灰
        turnFormGray(node, true);

        -- 停止当前格子一切动作
        pauseNode(node);

        -- 抛出事件暂停
        EventMgr.fire(event.GRID_LOCK_ACTION, {["pos"] = pos, });
    end

    -- 获取表现层当前格子
    local uiLevel = UIDungeonMgr.getCurLevel();
    local uiGrid  = uiLevel:getAllGrids()[pos];

    -- 若当前处于时间停止状态
    if uiLevel.isTimeCeasing then
        return;
    end

    -- 已处于石化状态
    if uiGrid:getIsInStone() then
        return;
    end

    -- 先打上标记
    uiGrid:setIsInStone(true);
    if noEffect then
        playStone(uiGrid:getMonsterNode());

        return;
    end

    local uiDungeonMain = UIMgr:getCurrentScene():getFormByName("UIDungeonMain");
    local modelNode = findChildByName(uiDungeonMain.node, "CT2/model_node");
    local talk = findChildByName(uiDungeonMain.node, "CT2/talk");
    modelNode:setVisible(false);
    talk:setVisible(false);

    local monsterNode = uiGrid:getMonsterNode();
    monsterNode:setVisible(false);

    -- 短暂锁屏
    require "game/ui/form/common/UIBlackForm"
    local uiForm = UIBlackForm.create(2.7, function ()
        modelNode:setVisible(true);
        monsterNode:setVisible(true);
        talk:setVisible(true);
        playStone(uiGrid:getMonsterNode());
    end, UIMgr.TOP_MENU_ZORDER+1, uiGrid, content);

    UIMgr.getCurrentScene():addForm(uiForm);

    -- 特效
    playEffect(uiForm.monsterNode, 104, 0, 30);

    -- 播放烟雾
    performWithDelay(uiForm.node, function ()
        uiForm:playEffect(uiForm.monsterNode, "animation/open_block.csb", 0.2, nil, 1);
    end, 1.6);

    -- 播放石化
    performWithDelay(uiForm.node, function ()
        playStone(uiForm.monsterDisplay);
    end, 2);
end

-- 停止石化效果
function stopPetrified(pos)
    -- 获取表现层当前格子
    local uiLevel = UIDungeonMgr.getCurLevel();
    local uiGrid  = uiLevel:getAllGrids()[pos];

    -- 变为正常色
    turnFormGray(uiGrid:getMonsterNode(), false);

    -- 动作继续
    if uiGrid:getMonsterNode() ~= nil then
        resumeNode(uiGrid:getMonsterNode());
    end

    if uiGrid:getItemNode() ~= nil then
        resumeNode(uiGrid:getItemNode());
    end

    -- 打上标记
    uiGrid:setIsInStone(false);

    -- 设置时间停止需要重刷标记
    uiLevel:setTimeCeaseNeedRefresh(true);
end

-- 开始时间停止效果
function playTimeCeaseEffect()
    local uiLevel = UIDungeonMgr.getCurLevel();
    if uiLevel == nil then
        return;
    end

    -- 上下菜单例外
    local exceptionList = {"TOP", "CT2"};

    -- 变灰
    turnFormGray(uiLevel, true, exceptionList);

    -- 怪物和道具节点动作停止
    local grids = uiLevel:getAllGrids();
    for _, grid in pairs(grids) do

        -- 暂停动作
        if grid:getMonsterNode() ~= nil then
            pauseNode(grid:getMonsterNode());
        end

        if grid:getItemNode() ~= nil then
            pauseNode(grid:getItemNode());
        end
    end

    -- 暂停特效
    local uiForm = UIMgr:getCurrentScene():getFormByName("UIDungeonMain");
    if uiForm then
        for _, c in pairs(uiForm.panel:getChildren()) do
            for _, cl in pairs(c:getChildren()) do
                if cl.action then
                    cl.action:pause();
                end
            end
        end
    end

    uiLevel.isTimeCeasing = true;
    uiLevel:setTimeCeaseNeedRefresh(false);
end

-- 结束时间停止效果
function stopTimeCeaseEffect()
    local uiLevel = UIDungeonMgr.getCurLevel();
    if uiLevel == nil then
        return;
    end

    -- 上下菜单例外
    local exceptionList = {"TOP", "CT2"};

    -- 变正常色
    turnFormGray(uiLevel, false, exceptionList);
    AudioM.playFx("skill_jingzhi2");

    -- 怪物和道具节点动作继续
    local grids = uiLevel:getAllGrids();
    for _, grid in pairs(grids) do
        if grid:getMonsterNode() ~= nil then
            resumeNode(grid:getMonsterNode());

            if grid.monsterNode.bishopIcon then
                setGrayMode(grid.monsterNode.bishopIcon, true);
            end
        end

        if grid:getItemNode() ~= nil then
            resumeNode(grid:getItemNode());
        end
    end

    -- 特效继续
    local uiForm = UIMgr:getCurrentScene():getFormByName("UIDungeonMain");
    if uiForm then
        for _, c in pairs(uiForm.panel:getChildren()) do
            for _, cl in pairs(c:getChildren()) do
                if cl.action then
                    cl.action:resume();
                end
            end
        end
    end

    uiLevel.isTimeCeasing = false;
    uiLevel:setTimeCeaseNeedRefresh(false);

    -- 继续播完时间停止动画
    if uiLevel.timeCeaseEffect and uiLevel.timeCeaseEffect.action then
        uiLevel.timeCeaseEffect.action:setTimeSpeed(0.1);
        uiLevel.timeCeaseEffect.action:resume();
        uiLevel.timeCeaseEffect = nil;

        uiLevel.timeCeaseLabel:removeFromParent();
        uiLevel.timeCeaseLabel = nil;
    end
end

-- 检查时间停止效果
function checkTimeCeaseEffectForNode(node)
    if node == nil or UIDungeonMgr.getCurLevel() == nil then
        return;
    end

    if UIDungeonMgr.getCurLevel().isTimeCeasing then
        turnFormGray(node, true, {});
        pauseNode(node);
    end
end

-- 播放页签从上面往下掉的动画
function playTabEffect(tabs)
    -- 带窗口动画播放结束后，页签从上面往下掉
    for i = 1, #tabs do
        local tab = tabs[i];
        local pos = cc.p(tab:getPosition());
        local delay = cc.DelayTime:create(0.2 * (i - 1));
        local moveDown = cc.MoveTo:create(0.2, cc.p(pos.x, pos.y - 100 - 5));
        local moveBack = cc.MoveTo:create(0.2, cc.p(pos.x, pos.y - 100));
        local action = cc.Sequence:create(delay, moveDown, moveBack);
        tab:runAction(action);
    end
end

-----------------------------
-- 播放建筑强光特效
-- @param sourceNode 建筑格子
-- @param isWeak     是否使用较弱的光效
function playBuildingStrongLight(sourceNode, isWeak)
    if isWeak then
        playEffect(sourceNode, 1264, -18, 140, nil, 1.3);
        playEffect(sourceNode, 1263, -20, 130, nil, 1.3);
    else
        playEffect(sourceNode, 1261, -18, 140, nil, 1.3);
        playEffect(sourceNode, 1262, -20, 120, nil, 1.3);
    end
end

-- 播放黑市商人冒气光效
function playBlackMarketEffect(node, offsetX, offsetY)
    playEffect(node, 2021, offsetX, offsetY);
end

-- 圣骑士训练关卡，休息室
function playPaladinEffect(sourceNode)
    local dustEffectParent = findChildByName(sourceNode, "effect_node");
    -- 去掉节点上所有动画
    dustEffectParent:removeAllChildren();

    local uiCurLevel = UIDungeonMgr.getCurLevel();
    local imgNode = uiCurLevel:getBlackMask();

    -- 初始化黑色蒙版属性
    imgNode:setVisible(true);
    imgNode:setOpacity(0);
    imgNode:loadTexture("images/ui/common/mengban_black.png");
    imgNode:setCascadeOpacityEnabled(true);
    imgNode:setTouchEnabled(true);

    -- 图片渐入渐出
    local fadeOut = cc.FadeTo:create(1.5, 0);
    local fadeIn = cc.FadeTo:create(0.8, 230);
    local delay = cc.DelayTime:create(2);

    local callFunc = cc.CallFunc:create(function()
        imgNode:setVisible(false);
        imgNode:setOpacity(255);
        imgNode:setTouchEnabled(false);
        imgNode:loadTexture("images/ui/level/black_mask2.png");
    end);

    imgNode:runAction(cc.Sequence:create(fadeIn, delay, fadeOut, callFunc));
end

-- 播放元素圣坛提交失败光效
function playElemMagicFailEffect(color, direction, sourceNode)
    local fromPos = cc.p(sourceNode:getPosition());

    -- 所有充能特效
    local chargeEffects = { ["blue"] = 1317, ["green"] = 1318, ["red"] = 1316, ["yellow"] = 1319 };

    -- 待播放的充能特效
    local chargeEffectId = chargeEffects[color];
    print("当前播放ID = " .. chargeEffectId);

    -- 根据方向修正射线的起始位置
    if direction == "left" then
        local elemIconNode = findChildByName(sourceNode.icon, "left_node");
        fromPos = cc.pAdd(fromPos, cc.p(elemIconNode:getPosition()));
        fromPos = cc.pAdd(fromPos, cc.p(-70, -23));

        -- 1.播放充能特效
        playEffect(sourceNode, chargeEffectId, -37, 18);

    elseif direction == "right" then
        local elemIconNode = findChildByName(sourceNode.icon, "right_node");
        fromPos = cc.pAdd(fromPos, cc.p(elemIconNode:getPosition()));
        fromPos = cc.pAdd(fromPos, cc.p(-55, -28));

        -- 1.播放充能特效
        playEffect(sourceNode, chargeEffectId, 50, 25);

    elseif direction == "mid" then
        local elemIconNode = findChildByName(sourceNode.icon, "mid_node");
        fromPos = cc.pAdd(fromPos, cc.p(elemIconNode:getPosition()));
        fromPos = cc.pAdd(fromPos, cc.p(-59, -33));

        -- 1.播放充能特效
        playEffect(sourceNode, chargeEffectId, -10, 55);
    else
        trace("Effects", "未知的方向：%s", tostring(direction));
    end
end

-- 跳跃动画
-- @param node         跳动的节点
-- @param rangeX       X方向上的跳跃范围
-- @param rangeY       Y方向上的跳跃范围
-- @param rangeTime    跳跃时间变动范围
-- @param rangeHeight  跳跃的高度范围
-- @param shadow       阴影（没有的话就不播阴影动画）
-- @param shadowOffset 阴影位置调整（没有的话用默认位置）
function slimeJumpAnimation(node, rangeX, rangeY, rangeTime, rangeHeight, shadow, shadowOffset)
    -- 把锚点设置为下底边中心
    node:setAnchorPoint(cc.p(0.5, 0));
    -- 把影子放在节点的下方
    if shadow and shadowOffset then
        --shadow:setPosition(node:getPositionX() - shadowOffset[1], node:getPositionY() - shadowOffset[2]);
    elseif shadow then
        shadow:setPosition(node:getPositionX(), node:getPositionY() - 1);
    end

    local origionPos = cc.p(node:getPositionX(), node:getPositionY());

    local function jumpAnimation()
        local x = (nil ~= rangeX) and math.random(rangeX.x, rangeX.y) or math.random(-10, 10);
        local y = (nil ~= rangeY) and math.random(rangeY.x, rangeY.y) or math.random(-10, 10);

        local targetPos = cc.pAdd(origionPos, cc.p(x, y));
        local shadowTargetPos = targetPos;

        local jumps = math.ceil(math.abs(node:getPositionX() - targetPos.x) / 200);
        local height = rangeHeight and math.random(rangeHeight.x, rangeHeight.y) or math.random(5, 15);

        -- 计算跳跃次数
        local times = 1;

        -- 获取scale
        local scale = node:getScaleY();

        -- 设置跳跃时间
        local jumpTime = 0.2;

        -- 冈布奥待机动画
        local function idleAnimation()
            -- 判断方向(正右，负左)
            local origionPos2 = cc.p(node:getPosition());
            local direction = (targetPos.x - origionPos2.x > 0) and 1 or -1;
            -- 最大扭曲角度
            local MAX_SKEW_DEGREE = 5;
            -- 计算扭曲因子
            local skewFactor = math.floor(MAX_SKEW_DEGREE * (targetPos.x - origionPos.x) / 20);
            -- 先转向
            local oriScale = node:getScaleX();
            local xScale;
            if targetPos.x == origionPos2.x then
                -- 没有移动，保持原来的
                xScale = node:getScaleX();
            else
                xScale = direction*scale;
            end
            node:setScale(xScale, scale);

            -- 右上方向扭曲
            local skewRight = cc.EaseIn:create(cc.SkewBy:create(0.05, skewFactor, 0), 2.5);
            -- 压缩
            local compressDown1 = cc.EaseIn:create(cc.ScaleTo:create(0.025, xScale, scale-0.1), 2.5);
            -- 拉伸
            local stretchUp1 = cc.EaseOut:create(cc.ScaleTo:create(0.05, xScale, scale+0.1), 2.5);
            -- 起跳
            local jumpBy = cc.JumpTo:create(jumpTime, targetPos, 13, times);
            -- 左上方向扭曲
            local skewLeft = cc.EaseOut:create(cc.SkewBy:create(0.2, -skewFactor, 0), 2.5);
            -- 压缩
            local compressDown2 = cc.EaseOut:create(cc.ScaleTo:create(0.1, xScale, scale-0.05), 2.5);
            -- 站直
            local skewVertical = cc.EaseOut:create(cc.SkewBy:create(0.05, 0, 0), 2.5);
            -- 拉伸还原
            local stretchUp2 = cc.EaseOut:create(cc.ScaleTo:create(0.05, xScale, scale), 2.5);

            -- 根据方向进行跳跃
            node:stopAllActions();
            if targetPos.x - origionPos.x > 0 then
                node:runAction(cc.Sequence:create(compressDown1, skewRight, stretchUp1, jumpBy, compressDown2, skewLeft, stretchUp2, skewVertical));
            else
                node:runAction(cc.Sequence:create(compressDown1, skewLeft, stretchUp1, jumpBy, compressDown2, skewRight, stretchUp2, skewVertical));
            end

            -- 影子位置
            shadowTargetPos = targetPos;
            if shadowOffset then
                if oriScale * direction < 0 and targetPos.x ~= origionPos2.x then
                    -- 转向了
                    shadow:setPositionX(shadow:getPositionX() - 2 * direction * shadowOffset[1]);
                end
                if direction > 0 then
                    shadowTargetPos.x = targetPos.x - shadowOffset[1];
                else
                    shadowTargetPos.x = targetPos.x + shadowOffset[1];
                end
                shadowTargetPos.y = targetPos.y + shadowOffset[2];
            end
        end
        idleAnimation();

        -- 影子
        if shadow then
            local jumpBy2 = cc.JumpTo:create(jumpTime, cc.p(shadowTargetPos.x, shadowTargetPos.y), 0, times);
            shadow:stopAllActions();
            shadow:runAction(cc.Sequence:create(cc.DelayTime:create(0.28), jumpBy2));
        end

        -- 间隔随机时间2-8秒
        local r = rangeTime and math.random(rangeTime.x, rangeTime.y) or math.random(100, 300);
        performWithDelay(node, jumpAnimation, r / 100);
    end

    -- 间隔随机时间0.5-3秒
    local r = math.random(50, 300);
    performWithDelay(node, jumpAnimation, r / 100);
end

-- 放逐特效
function playSeckillEffect(targetNode)
    playEffect(targetNode, 3000, -105, 135);
end

-- 播放大预言术光效
function playPredictionEffect(callback)
    local effectParent = UIDungeonMgr.getCurLevel():getEffectParent();
    local node = cc.Node:create();
    local contentSize = effectParent:getContentSize();
    local w = contentSize.width;
    local h = contentSize.height;

    local function frameEventFunc(frame)
        if frame.getEvent and frame:getEvent() == "pilor" then
            local name = frame:getNode():getName();
            local index = frame:getFrameIndex();

            if index == 45 then
                -- 结束;
                if callback then
                    callback();
                end
                node:stopAllActions();
                node:removeAllChildren();
            end
        end
    end

    -- 翻书光效
    playEffect(node, 509, 0, 0, frameEventFunc);


    effectParent:addChild(node);
    node:setAnchorPoint(0.5, 0.5);
    node:setPosition(w/2, h/2);
end

-- 播放石柱禁魔光效
function playCrystalEffect(sourceNode)
    -- 光效配置
    local lightEffectMap = { 1197, 1198, 1199, 1200, 1201, 1208 };

    -- 创建石柱动画
    local effectMap = lightEffectMap;

    local fromPos = cc.p(sourceNode:getPosition());

    -- 是否暂停
    isPause = isPause or false;

    playActiveDoorEffect(sourceNode.gridData:getPos());

    -- 播放其他修饰特效
    local function playDecaration()
        local decaNode = cc.Node:create();

        -- 小石头上下飘动
        local rockLeftNode = cc.Node:create();
        playEffect(rockLeftNode, effectMap[5]);
        decaNode:addChild(rockLeftNode);

        local rockRightNode = cc.Node:create();
        playEffect(rockRightNode, effectMap[4]);
        decaNode:addChild(rockRightNode);

        -- 闪烁特效
        local shinningNode = cc.Node:create();
        playEffect(shinningNode, effectMap[3]);
        decaNode:addChild(shinningNode);

        -- 灯光特效
        local lightNode = cc.Node:create();
        playEffect(lightNode, effectMap[2]);
        decaNode:addChild(lightNode);

        sourceNode.item.decaNode = decaNode;
        sourceNode.item:addChild(decaNode);
    end

    -- 如果是暂离回来，则立即显示装饰特效，否则需要等播放光圈特效后才显示
    local delay = iif(isPause == true, 0, 0.3);
    performWithDelay(sourceNode, playDecaration, delay);
end

-- 播放激活传送门的光效
function playActiveDoorEffect(pos)
    -- 四个节点的位置
    local sourcePos = {12, 22, 24, 14, 18};
    -- 获取所有格子
    local grids = UIDungeonMgr:getCurLevel():getAllGrids();

    -- 所有充能特效
    local chargeEffectId = 2039;

    -- 所有射线特效
    local lineEffectId = 2043;

    -- 所有爆炸特效
    local boomEffectId = 2047;

    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();

    local sourceNode = grids[pos];
    local targetNode = grids[18];

    local fromPos = cc.p(sourceNode:getPosition());
    fromPos = cc.pAdd(fromPos, cc.p(0, 40));

    local node = cc.Node:create();

    effectParent:addChild(node);

    local doorNode = cc.Node:create();

    -- 目标位置
    local toPos = cc.p(targetNode:getPosition());

    trace("Effect", "[playElemMagicEffect]fromPosX = %d, fromPosY = %d", fromPos.x, fromPos.y);
    trace("Effect", "[playElemMagicEffect]toPosX = %d, toPosY = %d", toPos.x, toPos.y);

    -- 2.创建射线轨迹
    -- 计算两点间的距离
    local distance = cc.pGetDistance(fromPos, toPos);

    -- 计算水平射线需要旋转的角度
    -- 由于setRotation是顺时针旋转的，因此需要取反
    local angle = -calcPointAngle(fromPos, toPos);

    -- 射线光效的长度
    local unitLength = 126;

    local scaleX = distance / unitLength;

    trace("Effects", "angle = %d, distance = %d, scaleX = %f", angle, distance, scaleX);

    node:setScaleX(scaleX);

    node:setScaleY(2);

    node:setPosition(cc.p(fromPos.x, fromPos.y));  -- 修正一下射线光效的起始位置
    node:setRotation(angle);

    doorNode:setPosition(cc.p(toPos.x, toPos.y));
    effectParent:addChild(doorNode);
    -- 播放射线光效
    playEffect(node, lineEffectId);

    playEffect(doorNode, chargeEffectId);

    playDoorShakeEffect(targetNode, 0.05, 3, 3);
    -- 射线攻击播放时长
    local duration = 0.6;

    local function finishLineEff()
        effectParent:removeChild(node);
        effectParent:removeChild(doorNode);
    end

    performWithDelay(sourceNode, finishLineEff, duration);
end

-- 追击炮弹特效
function playMortarShellsEffect(posList)
    -- 参数不符合要求
    if type(posList) ~= "table" or #posList <= 0 then
        return;
    end

    -- 锁屏
    wait(2.2);

    -- 获取待打击目标数量
    local targetNum = #posList;

    -- 发射光效的中心应在屏幕的正中央
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    local width = AlignM.frameSize.width;
    local height = AlignM.frameSize.height;

    -- 获取光效节点
    local node = cc.Node:create();
    effectParent:addChild(node);

    -- 发射炮弹
    local offset = 50;
    playEffect(node, 3019, width / 2 - offset, height / 2 + offset);

    -- 命中爆炸
    -- 哈希表已经是散列表了，不做随机选择打击目标了
    for _, pos in pairs(posList) do
        -- 获取对应目标位置
        local uiGrid = UIDungeonMgr.getCurLevel().grids[pos];

        -- 命中爆炸
        local function hitAndBomb()
            local node1 = cc.Node:create();
            effectParent:addChild(node1);
            local worldPos = uiGrid:convertToWorldSpace(cc.p(0, 0));
            local sourcePos = effectParent:convertToNodeSpace(worldPos);
            node1:setPosition(sourcePos);
            node1:setLocalZOrder(100000);

            -- 光效
            playEffect(node1, 3020);

            -- 震动
            performWithDelay(uiGrid.node, function() playShakeEffect(UIDungeonMgr:getCurLevel(), 0.03, 8, 8); end, 1.1);
        end

        -- 延时一个随机时间,播放命中光效
        local r = math.random(50, 100);
        performWithDelay(uiGrid.node, hitAndBomb, r / 100);
    end
end

-- 银之手
function attackStealingEffect(targetNode)
    -- 先锁屏
    wait(1.5);

    -- 获得出战英雄的位置
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    local uiForm = UIMgr:getCurrentScene():getFormByName("UIDungeonMain");
    local monsterNode = findChildByName(uiForm.node, "CT2/model_node");
    local worldPos = monsterNode:convertToWorldSpaceAR(cc.p(0, 0));
    local sourcePos = effectParent:convertToNodeSpace(worldPos);

    -- 获得起始位置
    local node = cc.Node:create();
    effectParent:addChild(node);

    -- 创建链条
    local node = ccui.ImageView:create();
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    effectParent:addChild(node);
    node:loadTexture("images/effects/slime_skill_silver_hand1.png");
    node:setPosition(sourcePos);

    -- 锚点设置为下底边中心
    node:setAnchorPoint(cc.p(0.5, 0));

    -- 记录链条原始数据
    local oldScaleY = node:getScaleY();
    local oldLength = node:getContentSize().height;

    -- 初始化链条长度
    local startScaleY = 0.1;
    node:setScaleY(startScaleY);

    -- 初始化爪子到链条顶端
    local node2 = createEffectAnim(3026);
    effectParent:addChild(node2);
    node2:play(0, 3, true);
    node2:setPosition(sourcePos);

    -- 把起始位置和目标位置坐标全部统一下effectParent的坐标系下
    local fromPos = sourcePos;
    worldPos = targetNode:convertToWorldSpaceAR(cc.p(0, 0));
    local toPos = effectParent:convertToNodeSpace(worldPos);

    -- 计算距离
    local distance = cc.pGetDistance(fromPos, toPos);

    -- 计算伸缩比
    local scaleStretch = distance * oldScaleY / oldLength;

    -- 计算旋转角度，并旋转链条
    -- 通过calcPointAngle计算出来的是与水平方向的夹角，而图片默认是垂直方向的，因此还要减去90度
    -- 由于setRotation是顺时针旋转的，因此需要取反
    local angle = -(calcPointAngle(fromPos, toPos) - 90);
    node:setRotation(angle);
    node2:setRotation(angle);

    -- 拉伸链条部分
    local stretchUp1 = cc.EaseIn:create(cc.ScaleTo:create(0.2, node:getScaleX(), scaleStretch), 0.3);
    local moveTo1 = cc.EaseIn:create(cc.MoveTo:create(0.2, toPos), 0.3);

    -- 播放打击光效部分
    local stretchUp2 = cc.ScaleTo:create(0.6, node:getScaleX(), scaleStretch);
    local delay2 = cc.MoveTo:create(0.05, toPos);
    local moveTo2 = cc.MoveTo:create(0.55, toPos);
    local effect2 = cc.CallFunc:create(function() node2:play(3, 6, false, 0.5); end);
    local effect3 = cc.CallFunc:create(function()
                        performWithDelay(node2, function()
                            playEffect(node2, 3027, 0, 30);
                        end, 0.2)
                    end);
    local callFunc2 = cc.Spawn:create(moveTo2, effect2, effect3);

    -- 收缩链条部分
    local compressDown3 = cc.EaseOut:create(cc.ScaleTo:create(0.2, node:getScaleX(), startScaleY), 0.3);
    local MoveTo3 = cc.EaseOut:create(cc.MoveTo:create(0.2, fromPos), 0.3);

    -- 淡出部分
    local fadeOut4 = cc.FadeOut:create(0.8);
    local MoveTo4 = cc.MoveTo:create(0.8, fromPos);
    local effect4 = cc.CallFunc:create(function() node2:play(18, 25, false); end);
    local callFunc4 = cc.Spawn:create(MoveTo4, effect4);
    local remove4 = cc.CallFunc:create(function() effectParent:removeChild(node2); end);

    -- 银之手发动
    node:runAction(cc.Sequence:create(stretchUp1, stretchUp2, compressDown3, fadeOut4));
    node2:runAction(cc.Sequence:create(moveTo1, delay2, callFunc2, MoveTo3, callFunc4, remove4));
end

-- 货币战争
function searchPointDamageEffect(posList)
    -- 先锁屏
    wait(2);

    -- 发射速度
    local speed = 1500;

    -- 发射频率
    local frequency = 1;

    -- 总播放时间
    local totalTime = 3;

    -- 轮次(所有被打击的目标不重复的全部被攻击过一次之后即为一轮)
    local round = #posList == 1 and 1 or 2;

    -- 每轮的时间
    local timePerRound = totalTime / 2;

    -- 获取发射位置
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    local uiForm = UIMgr:getCurrentScene():getFormByName("UIDungeonMain");
    local monsterNode = findChildByName(uiForm.node, "CT2/model_node");
    local worldPos = monsterNode:convertToWorldSpaceAR(cc.p(0, 0));
    local fromPos = effectParent:convertToNodeSpace(worldPos);

    -- 创建光效节点
    local effectNode = cc.Node:create();
    effectParent:addChild(effectNode);

    -- 粗略估算打击范围
    local width = 10;
    local height = 10;

    local delay = 0;

    -- 按轮次开始播放特效
    for curRound = 1, round do
        -- 单轮次的时间片指针
        local timePoint = 0.1;

        -- 先打乱下顺序
        local randomList = {};
        local amount = #posList;
        for index = 1, amount do
            index = math.random(1, amount);
            table.insert(randomList, posList[index]);
            table.remove(posList, index);
            amount = amount - 1;
        end
        posList = randomList;

        -- 遍历目标节点，开始打击
        for index, pos in pairs(posList) do
            -- 随机分配单轮次的时间
            timePoint = math.random(timePoint*10, timePerRound*10);
            timePoint = timePoint / 10;

            -- 获得目标节点
            local uiGrid = UIDungeonMgr.getCurLevel().grids[pos];

            -- 把目标位置坐标转换为effectPrarent的坐标系
            local world = uiGrid.node:convertToWorldSpaceAR(cc.p(0, 0));
            local toPos = effectParent:convertToNodeSpace(world);

            -- 计算分配时间片内受打击的次数
            local times = math.ceil(timePoint * frequency);

            -- 获取下次打击目标的位置
            local nextPos = posList[index + 1];
            local nextTarget, nextWorld, nextToPos;
            if nextPos then
                nextTarget = UIDungeonMgr.getCurLevel().grids[nextPos];
                nextWorld = nextTarget.node:convertToWorldSpaceAR(cc.p(0, 0));
                nextToPos = effectParent:convertToNodeSpace(worldPos);
            end

            -- 飞行效果
            local xPoint = 0;
            local function moveEffect()
                -- 克隆投掷物
                local node1 = ccui.ImageView:create();
                local iconPath = getSmallAttribIconPath("search_point");
                node1:loadTexture(iconPath);
                node1:setScale(0.8);

                effectParent:addChild(node1);
                node1:setPosition(fromPos);

                -- 实现打击位置不固定
                local posOffset = cc.p(0, 0);
                if nextToPos then
                    -- 如果有下一个打击目标，那么每次打击点的X坐标朝着下一个目标的方向随机移动
                    xPoint = (nextToPos.x - toPos.x > 0) and math.random(xPoint, width / 2) or math.random(-width / 2, xPoint);
                    local yOffset = math.random(-height / 2, height / 2);
                    posOffset = cc.p(xPoint, yOffset);
                else
                    -- 没有下一个打击目标,那么直接在矩形范围内随机取点
                    xOffset = math.random(-width / 2, width / 2);
                    yOffset = math.random(-height / 2, height / 2);
                    posOffset = cc.p(xOffset, yOffset);
                end

                toPos = cc.pAdd(toPos, posOffset);

                -- 转向
                local angle = -(calcPointAngle(fromPos, toPos) - 90);
                node1:setRotation(angle);

                -- 计算距离
                local distance = cc.pGetDistance(fromPos, toPos);

                -- 单次移动时间
                local time = distance / speed;

                -- 移动
                local scaleTo = cc.ScaleTo:create(time, 1.5);
                local moveTo = cc.MoveTo:create(time, toPos);
                local callFunc1 = cc.Spawn:create(scaleTo, moveTo);

                -- 中心
                toPos = cc.pAdd(toPos, cc.p(posOffset.x + 50, posOffset.y - 50));

                -- 打击
                local effect = cc.CallFunc:create(function() playEffect(effectNode, 1104, toPos.x, toPos.y, nil, 0.5); end);
                local destory = cc.CallFunc:create(function() effectParent:removeChild(node1); end);
                local callFunc2 = cc.Spawn:create(effect, destory);

                -- 货币战争
                node1:runAction(cc.Sequence:create(callFunc1, callFunc2));
            end

            -- 打击
            for i = 1, times do
                delay = delay + 1;
                performWithDelay(uiForm.node, moveEffect, 0.2 * (delay - 1));
            end
        end
    end
end

-- 大闹天宫
function naughtyMonkeyEffect()
    -- 锁屏
    wait(2);

    -- 获取光效父节点
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();

    -- 创建抛出棒子节点
    local node1 = cc.Node:create();
    local contentSize = effectParent:getContentSize();
    effectParent:addChild(node1);
    node1:setPosition(contentSize.width / 2, contentSize.height / 2);

    -- 创建旋转棒子节点
    local node2 = cc.Node:create();
    effectParent:addChild(node2);
    node2:setPosition(contentSize.width / 2, contentSize.height / 2);

    -- 抛出棒子
    local callFunc1 = cc.CallFunc:create(function() playEffect(node1, 3022, 0, 0); end);
    node1:runAction(cc.Sequence:create(callFunc1));

    -- 旋转
    local delay = cc.DelayTime:create(0.42);
    local callFunc2 = cc.CallFunc:create(function()
        playShakeEffect(UIDungeonMgr:getCurLevel(), 0.03, 8, 12);
        playEffect(node2, 3023, 0, 0);
    end);
    local delay2 = cc.DelayTime:create(1.5);
    local callFunc3 = cc.CallFunc:create(function() EventMgr.fire(event.PLAYER_MOVE); end);
    node2:runAction(cc.Sequence:create(delay, callFunc2, delay2, callFunc3));

    return 5;
end

-- 神圣重生
function holyReviveEffect(node)
    -- 锁屏
    wait(5);

    -- 隐藏原来的节点
    local modelNode = findChildByName(node, "CT2/model_node");
    modelNode:setVisible(false);

    UIMgr.getCurrentScene():removeFormByName("UIDungeonHpAlert");

    -- 黑色遮罩
    require "game/ui/form/common/UIBlackForm"
    local uiForm = UIBlackForm.create(6, function ()
        modelNode:setVisible(true);
    end, UIMgr.TOP_MENU_ZORDER+3, nil, content, true);
    UIMgr.getCurrentScene():addForm(uiForm);

    -- 逐渐变黑
    local mask = uiForm.mask;
    mask:setOpacity(0);
    local fadeIn1 = cc.FadeTo:create(1.5, 255);

    -- 复活光效
    local callFunc1 = cc.CallFunc:create(function() playEffect(uiForm.effectNode, 45, 0, 100); end);

    -- 等待
    local delay1 = cc.DelayTime:create(1.2);

    -- 英雄重生
    local callFunc2 = cc.CallFunc:create(function()
        -- 英雄出现
        uiForm:createHeroModel();

        -- 施法动作
        uiForm:switchHeroAction(HERO_ACTION_CAST_OTHER);

        -- 咆哮
        uiForm:showHeroTalk(getLocStr("holy_revive"));
    end);

    -- 等待
    local delay2 = cc.DelayTime:create(1.7);

    local callFunc3 = cc.CallFunc:create(function()
        -- 虚影
        local pet = ME.user:getActivePet();
        local petId = pet.classId;
        local iconIndex = pet:getIcon();
        uiForm:playActivePetShadowEffect(iconIndex);
    end);

    -- 逐渐变亮
    local fadeIn2 = cc.FadeTo:create(1.5, 0);

    -- 神圣重生
    mask:runAction(cc.Sequence:create(fadeIn1, callFunc1, delay1, callFunc2, delay2, callFunc3, fadeIn2));

end

-- 播放准心的特效
function playReticleEffect(node)
    local reticleNode = findChildByName(node, "CT/reticle_node");
    local reticleNode1 = findChildByName(reticleNode, "reticle1");
    local reticleNode2 = findChildByName(reticleNode, "reticle2");
    local reticleNode3 = findChildByName(reticleNode, "reticle3");
    local reticleNode4 = findChildByName(reticleNode, "reticle4");
    local reticleNode5 = findChildByName(reticleNode, "reticle5");

    -- 先暂停下所有准心图片的行为
    reticleNode:stopAllActions();
    reticleNode1:stopAllActions();
    reticleNode2:stopAllActions();
    reticleNode3:stopAllActions();
    reticleNode4:stopAllActions();
    reticleNode5:stopAllActions();

    -- 如果正在播光效，那就先把光效移除掉
    if reticleNode.effectNode then
        reticleNode:removeChild(reticleNode.effectNode);
        reticleNode.effectNode = nil;
    end

    reticleNode:setVisible(true);
    reticleNode1:setVisible(false);
    reticleNode2:setVisible(false);
    reticleNode3:setVisible(false);
    reticleNode4:setVisible(false);
    reticleNode5:setVisible(false);

    local function frameEventFunc(frame)
        if frame.getEvent and frame:getEvent() == "pilor" then
            local index = frame:getFrameIndex();

            if index == 32 then
                reticleNode.effectNode:setVisible(false);
                reticleNode1:setVisible(true);
                reticleNode2:setVisible(true);
                reticleNode3:setVisible(true);
                reticleNode4:setVisible(true);
                reticleNode5:setVisible(true);
                reticleNode1:setScale(1);
                reticleNode2:setScale(1);
                reticleNode3:setRotation(0);
                reticleNode4:setRotation(0);
                reticleNode5:setScale(1);

                -- 缩小后放大
                local delay1 = cc.DelayTime:create(0.4);
                local scaleDown1 = cc.ScaleTo:create(0.7, 0.7);
                local scaleUp1 = cc.ScaleTo:create(0.7, 1);

                -- 放大后缩小
                local delay2 = cc.DelayTime:create(0.4);
                local scaleUp2 = cc.ScaleTo:create(0.7, 1.3);
                local scaleDown2 = cc.ScaleTo:create(0.7, 1);

                -- 右转后左转
                local delay3 = cc.DelayTime:create(0.4);
                local rotateRight3 = cc.RotateBy:create(2, 90);
                local rotateLeft3 = cc.RotateBy:create(2, -90);

                -- 左转后右转
                local delay4 = cc.DelayTime:create(0.4);
                local rotateLeft4 = cc.RotateBy:create(2, -90);
                local rotateRight4 = cc.RotateBy:create(2, 90);

                -- 小幅度的放大后缩小
                local delay5 = cc.DelayTime:create(0.4);
                local scaleUp5 = cc.ScaleTo:create(1, 1.05);
                local scaleDown5 = cc.ScaleTo:create(1, 1);

                reticleNode1:runAction(cc.RepeatForever:create(cc.Sequence:create(delay1, scaleDown1, scaleUp1)));
                reticleNode2:runAction(cc.RepeatForever:create(cc.Sequence:create(delay2, scaleUp2, scaleDown2)));
                reticleNode3:runAction(cc.RepeatForever:create(cc.Sequence:create(delay3, rotateRight3, rotateLeft3)));
                reticleNode4:runAction(cc.RepeatForever:create(cc.Sequence:create(delay4, rotateLeft4, rotateRight4)));
                reticleNode5:runAction(cc.RepeatForever:create(cc.Sequence:create(delay5, scaleUp5, scaleDown5)));
            end
        end
    end

    local _, effectNode = playEffect(reticleNode, 1802, 0, 0, frameEventFunc);
    effectNode:setVisible(true);
    reticleNode.effectNode = effectNode;
end

-- 播放浮岛出现信息效果
function playAirShipInfoEffect(infoNode)
    local infoNodeList = {};

    local fireNode = findChildByName(infoNode, "fire_node");
    table.insert(infoNodeList, fireNode);

    local armorNode = findChildByName(infoNode, "armor_node");
    table.insert(infoNodeList, armorNode);

    local speedNode = findChildByName(infoNode, "speed_node");
    table.insert(infoNodeList, speedNode);

    local luckNode = findChildByName(infoNode, "luck_node");
    table.insert(infoNodeList, luckNode);

    --
    -- table.insert(infoNodeList, abilityNode);

    local campNode = findChildByName(infoNode, "camp_node");
    table.insert(infoNodeList, campNode);

    for i = 1, #infoNodeList do
        local node = infoNodeList[i];
        node:setScale(0.1);
        node:setOpacity(0);
        local delayTime = 0.1 * i;

        local delay     = cc.DelayTime:create(delayTime);
        local fadeIn    = cc.FadeIn:create(0.3);
        local scaleUp1  = cc.ScaleTo:create(0.3, 1.2);
        local scaleDown = cc.ScaleTo:create(0.1, 0.95);
        local scaleUp2  = cc.ScaleTo:create(0.05, 1);

        node:runAction(cc.Sequence:create(delay, cc.Spawn:create(fadeIn, scaleUp1), scaleDown, scaleUp2));
    end

    local abilityNode = findChildByName(infoNode, "ability_node");
    abilityNode:setScale(0.1);
    abilityNode:setOpacity(0);

    local delay     = cc.DelayTime:create(0.25);
    local fadeIn    = cc.FadeIn:create(0.3);
    local scaleUp1  = cc.ScaleTo:create(0.3, 1.2);
    local scaleDown = cc.ScaleTo:create(0.1, 0.95);
    local scaleUp2  = cc.ScaleTo:create(0.05, 1);
    abilityNode:runAction(cc.Sequence:create(delay, cc.Spawn:create(fadeIn, scaleUp1), scaleDown, scaleUp2));
end

-- 培养皿随机几个位置爆炸光效
function playBrokenDishEffect(effectNode)
    local rangePos = {7, 8 ,9, 12, 13, 14, 17, 18, 19};
    -- 随机选4个位置
    local targetPos = {};
    for i = 1, 4 do
        local index = math.random(#rangePos);
        table.insert(targetPos, rangePos[index]);
        table.remove(rangePos, index);
    end

    for index, pos in pairs(targetPos) do
        local grid = UIDungeonMgr.getCurLevel():getAllGrids()[pos];
        local delay = index * 0.15 + 0.3;

        performWithDelay(effectNode, function() playEffect(grid, 1233, 0, 20); end, delay);
    end

    return 1;
end

-- 时空裂痕光效
function whirlpoolEffect(parentNode, effectId, adjustX, adjustY)
    local _, effect = playEffect(parentNode, effectId, adjustX, adjustY);
    local children = effect:getChildren();
    local len = table.getn(children)
    for i = 1, len do
        local child = children[i];
        if child:getName() == "TX" then
            local children2 = child:getChildren();
            local len2 = table.getn(children2)
            for j = 1, len2 do
                local child2 = children2[j];
                -- 对layer 4节点下的sprite做高光和叠加效果
                -- 由于美术保存图片后亮光效果消失，故而转到程序实现
                setHighLightMode(child2, true, 1.3);
                child2:setBlendFunc(gl.SRC_COLOR, gl.ONE);
            end
        end
    end
end

-- 播放座钟发动陷阱
function deskclockTrapEffect(sourceNode, toPosition, effectParent, callback)
    local fromPos = cc.p(sourceNode:getPosition());
    print("fromPosX:"..fromPos.x..",fromPosY:"..fromPos.y);

    local toPos = toPosition;
    print("toPosX:"..toPos.x..",toPosY:"..toPos.y);

    local angle = cc.pGetAngle(fromPos, toPos);
    angle = radiansToDegrees(angle) + 210;

    trace("Effects", "弩车旋转角度：%d", angle);

    -- 金属发光
    local nodeLight = cc.Node:create();
    local effectChild = findChildByName(effectParent, "trap_light");
    if effectChild then
        effectParent:removeChild(effectChild);
    end

    nodeLight:setName("trap_light");
    playEffect(nodeLight, 1155, 0, 0);
    effectParent:addChild(nodeLight);

    -- 根据方向修正启动光效的位置
    nodeLight:setPosition(fromPos.x - 13, fromPos.y);

    -- 启动时间
    local prepareTime = 0.5;

    -- 启动时箭头往回拉伸的距离
    local offset = 10;

    -- 绘制箭头（需要飞出去）
    local arowImg = ccui.ImageView:create("images/ui/level/item/arow.png");

    -- 修正箭头的位置

    -- 右
    arowImg:setPosition(0, 0);
    -- local moveBy = cc.MoveBy:create(prepareTime/2, cc.p(offset, 0));
    -- arowImg:runAction(moveBy);

    local nodeArow = cc.Node:create();
    effectParent:addChild(nodeArow);
    nodeArow:setPosition(fromPos);
    nodeArow:addChild(arowImg);
    arowImg:setRotation(angle);

    -- 发射
    local function launch()
        local duration = math.max(cc.pGetDistance(fromPos, toPos)) / (2.5*speed);

        local callfunc = cc.CallFunc:create(function()
            effectParent:removeChild(nodeArow);

            -- 射中目标效果
            playEffect(targetNode, 1153, 0, 0);

            finishEffect();

            if callback then
                callback();
            end
        end);

        local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), 2.0);
        nodeArow:runAction(cc.Sequence:create(moveTo, callfunc));
    end
    launch();
    -- performWithDelay(sourceNode, launch, prepareTime);
end

-- 播放建筑虚影效果
-- 播放建筑消失特效(该接口适用于可淡出消失的建筑)
function playBuildingShadow(buidlingNode)
    if buidlingNode.icon == nil then
        return;
    end

    -- 整个节点逐渐缩小效果
    local imgNode = buidlingNode.item;

    -- 打开透明度设置传递开关
    local iconImg = buidlingNode.icon;
    if iconImg then
        iconImg:setCascadeOpacityEnabled(true);
    end
    imgNode:setCascadeOpacityEnabled(true);

    -- 图片渐出
    local fadeOut = cc.FadeOut:create(1);

    local callFunc = cc.CallFunc:create(function()
        imgNode:removeAllChildren();
        buidlingNode.icon = nil;
    end);


    local duration = 0.5;
    local scaleUp = cc.ScaleTo:create(duration, 1.8 * iconImg:getScale());
    local fadeOut = cc.FadeTo:create(duration, 0);
    local moveUp = cc.MoveBy:create(duration, cc.p(0, 60));
    imgNode:runAction(cc.Sequence:create(cc.Spawn:create(scaleUp, fadeOut, moveUp), callFunc));

    imgNode.isRemoving = true;
end

-- 原地跳跃动作
function jumpEffect(node, height)
    -- 把锚点设置为下底边中心
    node:setAnchorPoint(cc.p(0.5, 0));

    local scaleX = node:getScaleX();
    local scaleY = node:getScaleY();
    local posY = node:getPositionY();

    -- 起跳部分
    local moveUp = cc.EaseIn:create(cc.MoveTo:create(0.25, cc.p(node:getPositionX(), height)), 0.25);

    -- 下降部分
    local moveDown = cc.EaseOut:create(cc.MoveTo:create(0.25, cc.p(node:getPositionX(), posY)), 0.25);

    -- 还原
    local compressDown2 = cc.EaseOut:create(cc.ScaleTo:create(0.2, scaleX, scaleY-0.2), 2.5);
    local stretchUp2 = cc.EaseOut:create(cc.ScaleTo:create(0.1, scaleX, scaleY), 2.5);

    -- 跳跃
    node:runAction(cc.RepeatForever:create(cc.Sequence:create(moveUp, moveDown, compressDown2, stretchUp2)));
end

-- boss通用受DEBUF影响的特效
function bossRecieveDebuf(node, callback)
    local posList = {
        { cc.p(-100, 0), cc.p(80, 80), },
        { cc.p(40, -50), },
        { cc.p(50, 60), },
        { cc.p(-10, 30), },
        { cc.p(-110, 90), },
    };

    local num = 0;
    for i, pos in pairs(posList) do
        for _, p in pairs(pos) do
            performWithDelay(node, function() playEffect(node, 803, p.x, p.y); end, i * 0.3);
            num = num + 1;
        end
    end

    performWithDelay(node, function()
        if callback then callback(); end
    end, (num - 2) * 0.3);
end

-- 播放石柱禁魔光效
function playDarkForbidMagicEffect(sourceNode, isPause)
    -- 光效配置
    local darkEffectMap  = { 1192, 1193, 1194, 1195, 1196, 1209 };

    -- 创建石柱动画
    local effectMap = darkEffectMap;

    local fromPos = cc.p(sourceNode:getPosition());

    -- 是否暂停
    isPause = isPause or false;

    -- 石柱模型动画
    local animation = sourceNode.item.animation;

    if isPause then
        animation:gotoFrameAndPause(60);
    else
        animation:play(0, 60);

        -- 光圈音效
        AudioM.playFx("ward_lifted");

        -- 由于石柱的光圈要在所有格子之上，因此不能将其放在格子上，只能放到特效节点上
        local effectParent = UIDungeonMgr.getCurLevel():getEffectParent();
        local node = cc.Node:create();
        local circleAnim = createEffectAnim(effectMap[6]);
        circleAnim:play(0, 60);
        node.circleAnim = circleAnim;
        node:setName("stone_circle_anim");
        node:setPosition(cc.pAdd(fromPos, cc.p(0, 0)));
        node:addChild(circleAnim);
        effectParent:addChild(node);
    end

    -- 播放其他修饰特效
    local function playDecaration()
        local decaNode = cc.Node:create();

        -- 小石头上下飘动
        local rockLeftNode = cc.Node:create();
        playEffect(rockLeftNode, effectMap[5]);
        decaNode:addChild(rockLeftNode);

        local rockRightNode = cc.Node:create();
        playEffect(rockRightNode, effectMap[4]);
        decaNode:addChild(rockRightNode);

        -- 闪烁特效
        local shinningNode = cc.Node:create();
        playEffect(shinningNode, effectMap[3]);
        decaNode:addChild(shinningNode);

        -- 灯光特效
        local lightNode = cc.Node:create();
        playEffect(lightNode, effectMap[2]);
        decaNode:addChild(lightNode);

        sourceNode.item.decaNode = decaNode;
        sourceNode.item:addChild(decaNode);

        local function showAlert()
            -- 显示石柱警戒
            AlertStateM.setState(AlertStateM.ALERT_STATE_DARK_STONE);
        end
        performWithDelay(sourceNode, showAlert, 1.5);
    end

    -- 如果是暂离回来，则立即显示装饰特效，否则需要等播放光圈特效后才显示
    local delay = iif(isPause == true, 0, 0.3);
    performWithDelay(sourceNode, playDecaration, delay);
end

-- 暗之石柱关闭特效
function playDarkStoneCloseEffect(sourceNode)
    local animation = sourceNode.item.animation;
    if not animation then
        return;
    end
    -- 播放石柱关闭动画
    animation:play(64, 102);

    -- 光圈音效
    AudioM.playFx("ward_aperture");

    local effectParent = UIDungeonMgr.getCurLevel():getEffectParent();
    local circleNode = effectParent:getChildByName("stone_circle_anim");
    if circleNode == nil then
        -- 光圈动画尚未创建，则先创建
        local effectId = 1209;
        local fromPos = cc.p(sourceNode:getPosition());

        circleNode = cc.Node:create();
        local circleAnim = createEffectAnim(effectId);
        circleAnim:play(0, 60);
        circleNode.circleAnim = circleAnim;
        circleNode:setName("stone_circle_anim");
        circleNode:setPosition(cc.pAdd(fromPos, cc.p(0, 0)));
        circleNode:addChild(circleAnim);
        effectParent:addChild(circleNode);
    end

    circleNode.circleAnim:play(64, 102);

    -- 将修饰特效移除
    local function removeDecaration()
        if circleNode.circleAnim ~= nil then
            circleNode.circleAnim:setVisible(false);
        end

        -- 移除光圈特效
        effectParent:removeChild(circleNode);

        -- 移除石柱警戒
        AlertStateM.clearState(AlertStateM.ALERT_STATE_DARK_STONE);
    end

    if effectParent.stoneCloseNode == nil then
       effectParent.stoneCloseNode = cc.Node:create();
       effectParent:addChild(effectParent.stoneCloseNode);
    end

    performWithDelay(effectParent.stoneCloseNode, removeDecaration, 0.9);
end

-- 石柱爆炸
function playDarkStoneBlast(sourceNode)
    wait(2);
    local uiCurLevel = UIDungeonMgr.getCurLevel();
    local imgNode = uiCurLevel:getBlackMask();
    local effectParent = uiCurLevel:getEffectParent();

    -- 隐藏原来的节点
    local oriModelNode = findChildByName(uiCurLevel.node, "CT2/model_node");
    oriModelNode:setVisible(false);

    -- 初始化黑色蒙版属性
    imgNode:setVisible(true);
    imgNode:setOpacity(0);
    imgNode:loadTexture("images/ui/common/mengban_black.png");
    imgNode:setCascadeOpacityEnabled(true);
    imgNode:setTouchEnabled(true);

    -- 图片渐入渐出
    local fadeOut = cc.FadeTo:create(1.5, 0);
    local fadeIn = cc.FadeTo:create(0.8, 230);
    local delay = cc.DelayTime:create(2);

    local callFunc = cc.CallFunc:create(function()
        imgNode:setVisible(false);
        imgNode:setOpacity(255);
        imgNode:setTouchEnabled(false);
        imgNode:loadTexture("images/ui/level/black_mask2.png");
    end);

    imgNode:runAction(cc.Sequence:create(fadeIn, delay, fadeOut, callFunc));

    effectParent:removeAllChildren();
    local node = cc.Node:create();
    local item = cc.Node:create();
    node:setPosition(cc.pAdd(cc.p(sourceNode:getPosition()), cc.p(0, 20)));

    -- 原节点清空
    sourceNode.item:removeAllChildren();
    -- 隐藏cd
    local cdNode = findChildByName(sourceNode.node, "cd_node");
    cdNode:setVisible(false);

    -- 光柱模型
    local effectId = 1192;
    local animation = createEffectAnim(effectId);
    animation:gotoFrameAndPause(0);
    item.animation = animation;
    item:addChild(animation);
    node.item = item;
    node:addChild(item);

    -- 光柱动画
    playDarkForbidMagicEffect(node);
    effectParent:addChild(node);

    -- 英雄
    local heroPos = UIDungeonMgr.getCurLevel():getHeroPos();
    local modelNode = cc.Node:create();
    local headImg = cc.Node:create();
    modelNode:setPosition(cc.pAdd(heroPos, cc.p(0, 0)));
    headImg:setPosition(cc.pAdd(heroPos, cc.p(0, 0)));
    effectParent:addChild(modelNode);
    effectParent:addChild(headImg);

    -- 获取当前出战英雄
    local pet = ME.user:getActivePet();
    local classId = pet.classId;
    local petInfo = PetM.queryModel(pet:getModelId());

    local iconPath = getHeroIconPath(pet:getIcon());
    effectParent.heroNode = createHeroModel(pet:getModelId(), headImg, modelNode, petInfo["model_scale"], iconPath);

    -- 爆炸
    local function blast()
        -- 爆炸光效
        playEffect(node, 1233, 0, 20);

        local function delay()
            playEffect(effectParent.heroNode, 1233);
            local heroDisplay = findChildByName(effectParent.heroNode, "HeroDisplay");
            if heroDisplay then
                heroDisplay:switchAction(HERO_ACTION_DAMAGED);
            end
        end
        performWithDelay(sourceNode, delay, 0.5);

        local function disappear()
            effectParent:removeAllChildren();
            oriModelNode:setVisible(true);
            -- 移除石柱警戒
            AlertStateM.clearState(AlertStateM.ALERT_STATE_DARK_STONE);
        end
        performWithDelay(effectParent, disappear, 1.2);
    end
    performWithDelay(sourceNode, blast, 1);

    return 2.2;
end

-- 播放岩浆障碍消除特效
function playMagmaObstacleEffect(sourceNode)
    -- 光圈音效
    AudioM.playFx("ward_aperture");

    local effectParent = UIDungeonMgr.getCurLevel():getEffectParent();
    local fromPos = cc.p(sourceNode:getPosition());
    circleNode = cc.Node:create();
    local circleAnim = createEffectAnim(1208);
    circleAnim:play(0, 60);
    circleNode.circleAnim = circleAnim;
    circleNode:setPosition(cc.pAdd(fromPos, cc.p(0, -30)));
    circleNode:addChild(circleAnim);
    effectParent:addChild(circleNode);
end

-- 播放天空活动隐藏BOSS特效
function playSkyActivityHideBossEffecy(node)
    if node.imageView then
        node.imageView:removeFromParent();
        node.imageView = nil;
    end

    -- 克隆一张新的飞艇图片出来，用来处理放大效果
    local imageView = node:clone();
    imageView:setVisible(false);
    node:getParent():addChild(imageView);
    node.imageView = imageView;

    if not node.initPos then
        node.initPos = cc.p(node:getPositionX(), node:getPositionY());
    end

    node:stopAllActions();

    -- 飞艇抖动
    local pos = node.initPos;
    local duration = 0.05;
    local offset = 2;
    local moveLeft = cc.MoveTo:create(duration, cc.p(pos.x-offset, pos.y));
    local moveDown = cc.MoveTo:create(duration, cc.p(pos.x, pos.y-offset));
    local moveRight = cc.MoveTo:create(duration, cc.p(pos.x+offset, pos.y));
    local moveUp = cc.MoveTo:create(duration, cc.p(pos.x, pos.y+offset));
    local delay1 = cc.DelayTime:create(1);
    local callFunc1 = cc.CallFunc:create(function()
        -- 震动结束后回到初始坐标
        node:setPosition(pos);

        local initScale = node:getScale();
        local initOpacity = node:getOpacity();
        local delay     = 0.5;
        local delay2 = cc.DelayTime:create(1);
        local scaleTo   = cc.ScaleTo:create(delay, 1.5);
        local fadeOut   = cc.FadeOut:create(delay);
        local callFunc2  = cc.CallFunc:create(function ()
                imageView:setVisible(true);
                imageView:setScale(initScale);
                imageView:setOpacity(initOpacity);
            end)

        imageView:runAction(cc.Sequence:create(callFunc2, cc.Spawn:create(scaleTo, fadeOut)));
    end);

    node:runAction(cc.RepeatForever:create(cc.Sequence:create(moveLeft, moveDown, moveRight,
        moveUp, moveLeft, moveDown, moveRight, moveUp, callFunc1, delay1)));
end

-- 播放石柱消失特效
function playCannonDisappear(sourceNode)
    local animation = sourceNode.item.animation;

    if animation == nil then
        return;
    end

    -- 播放淡出特效
    animation:play(104, 115, false, 0.1);

    local callFunc = cc.CallFunc:create(function()
        sourceNode.item:removeAllChildren();
    end);

    local scaleUp = cc.ScaleTo:create(0.1, 1);
    local scaleDown = cc.ScaleTo:create(0.75, 0.8);
    sourceNode.item:runAction(cc.Sequence:create(scaleUp, scaleDown, callFunc));
end

-- 魔镜吸收光效
function playMirrorEffect(node)
    playEffect(node, 3000, -30, 55, nil, 0.8);
end

-- 播放普通建筑爆炸效果
function playBuildingBombEffect(node)
    -- 震动
    playShakeEffect(node, 0.03, 3, 2);

    -- 播放爆炸效果
    performWithDelay(node, function()
        playEffect(node.item, 818);
    end, 0.3);

    -- 播放烟尘效果
    performWithDelay(node, function()
        if node.playEffect then
            node:playEffect(node, "animation/open_block.csb", 0.5, nil, 1);
        end
    end, 0.7);
end

-- 阿瓦隆巨炮攻击特效
function avalonCannonAttackEffect(grid)
    -- 先锁屏
    wait(2);

    -- 获取横纵坐标
    local x = grid.x;
    local y = grid.y;

    -- 获取源节点
    local f = UIMgr:getCurrentScene():getFormByName("UIDungeonMain");
    sourceNode = f.grids[grid.pos];

    -- 源节点作为坐标系的原点，先获取源节点在世界坐标系的坐标
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    local worldPos1 = sourceNode:convertToWorldSpaceAR(cc.p(0, 0));

    -- 转化到effectParent的坐标系下
    local localPos1 = effectParent:convertToNodeSpace(worldPos1);

    -- 格子间纵间距
    local gridOffset = 130;

    local avalonCannon = grid.avalon_cannon;

    -- 攻击距离
    local cannonDistance = AvalonCannonM.query(avalonCannon.classId, "init_range");

    -- 范围加成
    local prop = PropM.combine(avalonCannon, "attrib2", "attack_range");
    cannonDistance = PropM.apply(prop, cannonDistance);

    -- 源节点到目标节点的距离
    local distance = gridOffset * cannonDistance;

    -- 获取炮弹的图标路径
    local bulletPath = getLevelItemIconPath("bullet2");

    -- 等待时间
    local waitTime = 0.1;

    -- 巨炮攻击
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    local cannonNode = effectParent:getChildByName("avalonCannon");
    local animation = cannonNode.action;
    local attackFrame = AvalonCannonM.query(avalonCannon.classId, "attack_frame");
    animation:gotoFrameAndPlay(attackFrame[1], attackFrame[2], false);

    -- 对7个方向进行打击，Y轴正方向为竖直方向，源节点为坐标原点
    local targetPos;
    local offsetX, offsetY;
    local rotates = {0, 45, 90, 135, 225, 270, 315};

    -- 抽取次数
    local fetchTimes = 20;

    -- 抽取时间范围
    local maxTime = 1.2;

    -- 每次从方向列表里抽取一个方向
    -- 在0-1.5的时间范围内随机进行播放
    for i = 1, fetchTimes do
        local rotate = rotates[math.random(1, #rotates)];

        -- 绘制炮弹，起点为源节点
        local bulletImg = ccui.ImageView:create(bulletPath);
        local node = cc.Node:create();
        effectParent:addChild(node);
        node:addChild(bulletImg);
        node:setPosition(sourceNode:getPosition());

        bulletImg:setOpacity(0);

        offsetX = distance * math.sin(degreesToRadians(rotate));
        offsetY = distance * math.cos(degreesToRadians(rotate));

        -- 转换到effectParent的坐标系中
        worldPos2 = cc.pAdd(cc.p(node:getPositionX(), node:getPositionY()), cc.p(offsetX, offsetY));

        -- 在时间范围内随机进行打击
        local wait = math.random(1, maxTime * 10) / 10;
        local delayTime = cc.DelayTime:create(wait);

        local callfunc1 = cc.CallFunc:create(function()
            -- 移除子弹
            effectParent:removeChild(node);
        end);

        -- 移动
        local moveTo = cc.EaseOut:create(cc.MoveTo:create(distance/speed/2.5, worldPos2), 0.25);
        local callFunc2 = cc.CallFunc:create(function()
            -- 显示炮弹
            performWithDelay(sourceNode, function()
                if bulletImg and bulletImg.setOpacity then
                    bulletImg:setOpacity(255);
                end
            end, distance/speed/2.5/2.2)
        end);
        local spawn = cc.Spawn:create(moveTo, callFunc2);

        node:runAction(cc.Sequence:create(delayTime, spawn, callfunc1));
    end

    -- 获取攻击范围
    local attackRange = avalonCannon:getAttackRange();

    -- 按攻击范围整理下
    local arrangeData = {};
    local g;
    local range;
    for index, pos in ipairs(attackRange) do
        -- 距离
        g = DungeonM.getGridByPos(pos);
        range = math.abs(g.x - x) + math.abs(g.y - y);

        arrangeData[pos] = waitTime + 0.3*range;
    end

    -- 格子爆炸的光效
    for pos, delay in pairs(arrangeData) do
        -- 爆炸
        local function playDamage()
            local targetNode = f.grids[pos];

            playEffect(targetNode, 1006, 0, 0);
            targetNode:playEffect(targetNode, "animation/open_block.csb", 0.5, nil, 1);
        end

        performWithDelay(sourceNode, playDamage, delay);
    end

    -- 尝试触发开格子
    local openInterval = 0.1;
    EventMgr.fire(event.PLAYER_MOVE, { ["interval"] = openInterval, ["delayInfo"] = arrangeData,});
end

-- 播放通用的虚影效果
function playNormalShadowEffect(node, iconPath, offsetX, offsetY)
    offsetX = offsetX or 0;
    offsetY = offsetY or 0;

    -- 预先放置图片
    local imageList = {};
    local function createImage(index)
        local image = ccui.ImageView:create();

        image:loadTexture(iconPath);
        image:setAnchorPoint(cc.p(0.5, 0.5));
        image:setScale(1.5);
        image:setOpacity(255 * 0.9 / index);
        node:addChild(image);

        local size = image:getVirtualRendererSize();
        image:setPosition(size.width/2 + 20, size.height/2 - 15);
        table.insert(imageList, image);
    end

    createImage(1);
    createImage(2);

    -- 节点位置出现一个虚影，放大
    local index = 1;
    local function playEffect()
        local image = imageList[index];

        local duration = 0.5;
        local scaleUp = cc.ScaleTo:create(duration, 1.8 * image:getScale());
        local fadeOut = cc.FadeTo:create(duration, 0);
        local moveUp = cc.MoveBy:create(duration, cc.p(0, 60));
        local callFunc = cc.CallFunc:create(function()
            image:removeFromParent();
        end);
        image:runAction(cc.Sequence:create(cc.Spawn:create(scaleUp, fadeOut, moveUp), callFunc));

        index = index + 1;
    end

    playEffect();
    performWithDelay(node, playEffect, 0.2);
end

-----------------------------
-- 播放状态光效
-- @param node 目标格子
-- @param status 状态
function playCombatStatusEffect(node, status)
    if status ~= "shield_power" then
        return;
    end

    if node.csEffectNode then
        -- 先把原本的去掉
        node:removeChild(node.csEffectNode);
        node.csEffectNode = nil;
    end

    if status == "shield_power" then
        -- 充能护盾
        local _, effectNode = playEffect(node, 1342);

        -- 在光效区间循环
        local animation = effectNode.action;
        animation:gotoFrameAndPlay(6, 45, true);

        node.csEffectNode = effectNode;
        node.csEffectNode:setVisible(true);
        effectNode:setName(status);
    end
end

-----------------------------
-- 去掉状态光效
-- @param node 目标格子
-- @param status 状态
function removeCombatStatusEffect(node, status, forceClear)
    if not node.csEffectNode then
        return;
    end

    if node.csEffectNode:getName() == status or forceClear then
        -- 把节点去掉
        node:removeChild(node.csEffectNode);
        node.csEffectNode = nil;
    end
end

----------------------------
-- 电流干扰装置效果
function interfereDeviceEffect()
    local node = cc.Node:create();
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    effectParent:addChild(node);
    node:setName("interfere_device_effect");
    playEffect(node, 1339, 320, 500);
end

----------------------------
-- 电流干扰装置效果
function removeInterfereDeviceEffect()
    local node = cc.Node:create();
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    local node = effectParent:getChildByName("interfere_device_effect");
    if node then
        effectParent:removeChild(node);
    end
end

-- 毒气装置
function playGasDeviceAnimation(pos)
    local uiGrid = UIDungeonMgr:getCurLevel():getAllGrids()[pos];
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    local worldPos = uiGrid.node:convertToWorldSpaceAR(cc.p(0, 0));
    local targetPos = effectParent:convertToNodeSpace(worldPos);

    local _, node = playEffect(effectParent, 1340, targetPos.x + 3, targetPos.y + 10);
    setOpacity(node, 255);
    node:setName("gasDevice");
end

-- 阿瓦隆巨炮
function playAvalonCannonAnimation(pos)
    local uiGrid = UIDungeonMgr:getCurLevel():getAllGrids()[pos];
    local effectParent = UIDungeonMgr:getCurLevel():getEffectParent();
    local worldPos = uiGrid.node:convertToWorldSpaceAR(cc.p(0, 0));
    local targetPos = effectParent:convertToNodeSpace(worldPos);

    local _, node = playEffect(effectParent, 1343, targetPos.x-3, targetPos.y-5);
    setOpacity(node, 255);
    local animation = node.action;
    node:setName("avalonCannon");

    local child = node:getChildByName("cannon_effect");
    if child then
        child:removeFromParent();
    end

    -- 停止在最后一帧
    local idleFrame = AvalonCannonM.query(ME.user.avalon_cannon.classId, "idle_frame");
    animation:gotoFrameAndPlay(idleFrame[1], idleFrame[2], false);
end

-- 电椅效果
function playElectricChairEffect(effectId)
    local effect = effectId or 31;
    local heroPos = UIDungeonMgr.getCurLevel():getHeroPos();
    local effectParent = UIDungeonMgr.getCurLevel():getEffectParent();
    local node = cc.Node:create();
    local function delay()
        playEffect(node, effect, 0, 115, nil, 2.0);
    end
    effectParent:addChild(node);
    node:setPosition(heroPos);

    for i = 1, 3 do
        performWithDelay(node, delay, 0.3 * (i - 1));
    end
end

-- 远程的火球效果
function playBulletFlyEffect(pos, offset)
    local fromPos = UIDungeonMgr:getCurLevel():getMonsterPos(pos);
    local toPos = UIDungeonMgr:getCurLevel():getHeroPos();

    if type(offset) == "table" then
        fromPos = cc.pAdd(fromPos, cc.p(offset[1], offset[2]));
    end

    local duration = cc.pGetDistance(fromPos, toPos) / 2000;

    local effect = ccui.ImageView:create();
    effect:loadTexture(getLevelItemIconPath("bullet"));
    effect:setPosition(fromPos);
    local moveTo = cc.MoveTo:create(duration, toPos);
    local rotate = cc.RotateBy:create(duration, 360);

    local function stopAction()
        UIDungeonMgr:getCurLevel():getEffectParent():removeChild(effect);
    end

    local callfunc = cc.CallFunc:create(stopAction)

    UIDungeonMgr:getCurLevel():getEffectParent():addChild(effect);
    effect:runAction(cc.Sequence:create(cc.Spawn:create(moveTo, rotate), callfunc));

    return duration;
end

-- 播放建造消失效果
function playBuildingDisappearEffect(node, effect)
    if effect == "bomb" then
        -- 播放爆炸效果
        playBuildingBombEffect(node);
    elseif effect == "bonus" then
        -- 播放奖励特效
        buildingBonusEffect(node);
    end
end

-- 播放冈布奥穿上时装效果
function playCostumeWearEffect(parent, icon)
    local node = tolua.cast(parent, "cc.Node");
    if not node then
        return;
    end

    parent:stopAllActions();
    parent:removeAllChildren();

    -- 预先放置图片
    local imageList = {};
    local function createImage(index)
        local image = ccui.ImageView:create();
        image:loadTexture(getHeroIconPath(icon));
        image:setOpacity(255 * 0.9 / index);
        image:setPosition(image:getContentSize().width/2, image:getContentSize().height/2);
        parent:addChild(image);
        table.insert(imageList, image);
    end

    createImage(1);
    createImage(2);

    -- 英雄位置出现一个虚影，放大
    local index = 1;
    local function playEffect()
        local image = imageList[index];

        local duration = 0.5;
        local scaleUp = cc.ScaleTo:create(duration, 2.5 * image:getScale());
        local fadeOut = cc.FadeTo:create(duration, 0);
        local moveUp = cc.MoveBy:create(duration, cc.p(0, 20));
        local callFunc = cc.CallFunc:create(function()
            image:removeFromParent();
        end);
        image:runAction(cc.Sequence:create(cc.Spawn:create(scaleUp, fadeOut, moveUp), callFunc));
        index = index + 1;
    end

    playEffect();
    performWithDelay(parent, playEffect, 0.2);
end

-- 漂浮
function itemMoveEffect(node)
    node.oriPosX = node:getPositionX();
    node.oriPosY = node:getPositionY();
    local function moveItem()
        local function doItemMove()
            local offsetY = node.oriPosY + 35;
            local moveUp = cc.MoveTo:create(1, cc.p(node.oriPosX, offsetY));
            local moveDown = cc.MoveTo:create(1, cc.p(node.oriPosX, offsetY - 10));
            local function moveDone()
                if node then
                    performWithDelay(node, doItemMove, 0.2);
                end
            end

            local callFunc = cc.CallFunc:create(moveDone);
            node:runAction(cc.Sequence:create(moveUp, moveDown, callFunc));
        end

        if node and not node.isMoving then
            node.isMoving = true;
            performWithDelay(node, doItemMove, 0.1);
        end
    end

    if node then
        local delay = math.random(1, 50) / 100;
        performWithDelay(node, moveItem, delay);
    end
end

-- 宝藏飞入特效
function pickupTreasureEffect(sourceNode, iconNode, iconPath)
    -- 图标飞入到菜单右上角探索点处
    local originToPosX = iconNode:getPositionX();
    local originToPosY = iconNode:getPositionY();

    -- 目标位置先转成世界坐标
    local worldPos = iconNode:convertToWorldSpaceAR(cc.p(0, 0));
    --print("originToPosX:"..originToPosY..",toPosY:"..originToPosY..", world posx:"..worldPos.x..",posy:"..worldPos.y);

    -- 再转到相对于sourceNode的父节点的坐标
    local parentNode = sourceNode:getParent();
    local toPos = parentNode:convertToNodeSpace(worldPos);
    --print("toPosX:"..toPos.x..",toPosY:"..toPos.y);

    local fromPos = cc.p(sourceNode:getPosition());
    --print("fromPosX:"..fromPos.x..",fromPosY:"..fromPos.y);

    local duration = math.max(cc.pGetDistance(fromPos, toPos), minDis) / speed;

    local icon = ccui.ImageView:create();
    icon:loadTexture(iconPath);
    icon:setPosition(fromPos);
    icon:setLocalZOrder(10000);

    local effectParent = sourceNode:getParent();
    local callfunc = cc.CallFunc:create(function()
        effectParent:removeChild(icon);
    end);

    local moveTo = cc.EaseInOut:create(cc.MoveTo:create(duration, toPos), 2.0);
    local rotate = cc.RotateBy:create(duration, 180);
    local scaleTo = cc.ScaleTo:create(duration, 0.5);
    icon:runAction(cc.Sequence:create(cc.Spawn:create(moveTo,rotate,scaleTo), callfunc));
    effectParent:addChild(icon);
end

-- 跳跃动画
-- @param node         跳动的节点
-- @param targetPos    目标位置
-- @param callback     回调
function fateChainSearchEffect(node, targetPos, callback)
    -- 把锚点设置为下底边中心
    node:stopAllActions();
    node:setAnchorPoint(cc.p(0.5, 0));
    local origionPos = cc.p(node:getPositionX(), node:getPositionY());
    local movePos = cc.p(origionPos.x - 30, origionPos.y);

    local function jumpAnimation()
        -- 获取比例scale
        local scale = node:getScaleY();

        -- 冈布奥待机动画
        local function idleAnimation()
            -- 获取比例xScale
            local xScale = node:getScaleX();

            -- 压缩
            local compressDown1 = cc.EaseIn:create(cc.ScaleTo:create(0.025, xScale, scale-0.2), 2.5);
            -- 拉伸
            local stretchUp1 = cc.EaseOut:create(cc.ScaleTo:create(0.05, xScale, scale+0.1), 2.5);
            -- 起跳
            local jumpBy = cc.JumpTo:create(0.2, movePos, 15, 1);
            -- 压缩
            local compressDown2 = cc.EaseOut:create(cc.ScaleTo:create(0.1, xScale, scale-0.1), 2.5);
            -- 贝塞尔曲线
            local data = {cc.p(node:getPosition()),cc.p(200, 200), targetPos};
            local bezierBy2 = cc.BezierTo:create(0.6, data);
            local bezierBy = cc.EaseOut:create(bezierBy2, 2.5);

            -- 贝塞尔曲线跳跃部分压缩拉伸
            local compressDown3 = cc.EaseOut:create(cc.ScaleTo:create(0.15, xScale, scale-0.1), 2.5);
            local stretchUp3 = cc.EaseOut:create(cc.ScaleTo:create(0.45, xScale, scale+0.2), 2.5);
            local compressDown4 = cc.EaseOut:create(cc.ScaleTo:create(0.2, xScale, scale-0.15), 2.5);
            local stretchUp4 = cc.EaseOut:create(cc.ScaleTo:create(0.2, xScale, scale), 2.5);
            local delay = cc.DelayTime:create(0.175);
            local delay2 = cc.DelayTime:create(0.01);

            node:runAction(cc.Sequence:create(compressDown1, stretchUp1, jumpBy, compressDown2, bezierBy));
            node:runAction(cc.Sequence:create(delay, compressDown3, stretchUp3, compressDown4, stretchUp4, delay2, cc.CallFunc:create(callback)));
        end

        idleAnimation();
    end

    jumpAnimation();
end

-- @param node         跳动的节点
-- @param moveX        每次横向位移
-- @param jumpTimes    跳跃次数
-- @param callback     回调
function fateChainJumpEffect(node, moveX, jumpTimes, callback)
    -- 把锚点设置为下底边中心
    node:stopAllActions();
    node:setAnchorPoint(cc.p(0.5, 0));
    local origionPos = cc.p(node:getPositionX(), node:getPositionY());

    -- 剩余跳跃次数
    local leftTimes = jumpTimes;

    -- 获取scale
    local xScale = node:getScaleX();
    local yScale = node:getScaleY();

    local function _callback()
        if callback then
            callback();
        end
    end
    local controlTime = 0.20;
    local function jumpAnimation()
        local delayTime = controlTime + 0.05;
        -- 冈布奥待机动画
        local function idleAnimation()
            -- 目标位置
            local movePos = cc.p(node:getPositionX() - moveX, origionPos.y);
            -- 先转向
            local xScale = node:getScaleX();

            -- 压缩
            local compressDown1 = cc.EaseOut:create(cc.ScaleTo:create(0.05, xScale, yScale-0.05), 2.0);
            -- 拉伸
            local stretchUp = cc.EaseOut:create(cc.ScaleTo:create(controlTime/2 + 0.05, xScale, yScale+0.125), 2.0);
            -- 起跳
            local jumpBy = cc.JumpTo:create(controlTime, movePos, math.random(30, 30), 1);
            -- 压缩
            local compressDown2 = cc.EaseOut:create(cc.ScaleTo:create(controlTime/2 - 0.05, xScale, yScale-0.1), 2.0);

            -- 恢复
            local stretchUp2 = cc.EaseIn:create(cc.ScaleTo:create(0.1, xScale, yScale), 2.0);

            if leftTimes == jumpTimes then
                -- 最初
                delayTime = controlTime + 0.10;
                local delay = cc.DelayTime:create(0.05);
                node:runAction(cc.Sequence:create(delay, jumpBy));
                node:runAction(cc.Sequence:create(compressDown1, stretchUp, compressDown2));
            elseif leftTimes == 1 then
                -- 最后一次
                node:runAction(cc.Sequence:create(stretchUp, compressDown2, stretchUp2, cc.CallFunc:create(_callback)));
                node:runAction(jumpBy);
            else
                -- 中间部分
                node:runAction(cc.Sequence:create(stretchUp, compressDown2));
                node:runAction(jumpBy);
            end
        end
        idleAnimation();

        leftTimes = leftTimes - 1;
        if leftTimes > 0 then
            performWithDelay(node, jumpAnimation, delayTime);
        end
    end

    jumpAnimation();
end